Skip to content

Instantly share code, notes, and snippets.

@lardratboy
Last active October 3, 2025 05:30
Show Gist options
  • Save lardratboy/4339907ae35281496f8675de7d9f802d to your computer and use it in GitHub Desktop.
Save lardratboy/4339907ae35281496f8675de7d9f802d to your computer and use it in GitHub Desktop.
HTML/javascript tool for viewing 8 to 32 bit data as point cloud
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DataPrism - what's in your file?</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #fff;
background-color: #000;
}
#container {
position: absolute;
width: 100%;
height: 100%;
}
#dropZone {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
display: none;
background: rgba(0, 0, 0, 0.8);
color: white;
justify-content: center;
align-items: center;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 28px;
font-weight: 600;
text-align: center;
backdrop-filter: blur(10px);
border: 3px dashed rgba(79, 195, 247, 0.6);
box-sizing: border-box;
}
#dropZone.dragover {
background: rgba(79, 195, 247, 0.2);
border-color: #4fc3f7;
}
#dropZone .drop-content {
padding: 40px;
border-radius: 12px;
background: linear-gradient(145deg, rgba(20, 20, 20, 0.9), rgba(40, 40, 40, 0.8));
border: 2px solid rgba(79, 195, 247, 0.4);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
}
#dropZone .drop-icon {
font-size: 48px;
margin-bottom: 16px;
color: #4fc3f7;
}
#dropZone .drop-text {
font-size: 24px;
margin-bottom: 8px;
color: #4fc3f7;
}
#dropZone .drop-subtext {
font-size: 14px;
color: #ccc;
opacity: 0.8;
}
#controls {
position: absolute;
top: 15px;
left: 15px;
padding: 12px;
background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(40, 40, 40, 0.9));
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
z-index: 100;
width: 220px;
font-size: 12px;
transition: transform 0.3s ease, opacity 0.3s ease;
transform-origin: top left;
}
#controls.minimized {
transform: scale(0.1);
opacity: 0;
pointer-events: none;
}
#controls h3 {
margin: 0 0 8px 0;
font-size: 14px;
font-weight: 600;
color: #4fc3f7;
text-align: center;
border-bottom: 1px solid rgba(79, 195, 247, 0.3);
padding-bottom: 6px;
position: relative;
}
#minimizeButton {
position: absolute;
top: -2px;
right: 0;
background: none;
border: none;
color: #4fc3f7;
font-size: 16px;
cursor: pointer;
padding: 2px 4px;
border-radius: 3px;
transition: background-color 0.2s ease, transform 0.2s ease;
line-height: 1;
width: 24px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
#minimizeButton:hover {
background-color: rgba(79, 195, 247, 0.2);
transform: scale(1.1);
}
#restoreButton {
position: absolute;
top: 15px;
left: 15px;
background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(40, 40, 40, 0.9));
backdrop-filter: blur(10px);
border: 1px solid rgba(79, 195, 247, 0.5);
border-radius: 50%;
color: #4fc3f7;
font-size: 18px;
cursor: pointer;
padding: 8px;
width: 40px;
height: 40px;
display: none;
align-items: center;
justify-content: center;
z-index: 101;
transition: all 0.3s ease;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
#restoreButton:hover {
background: linear-gradient(145deg, rgba(79, 195, 247, 0.2), rgba(79, 195, 247, 0.1));
border-color: #4fc3f7;
transform: scale(1.05);
box-shadow: 0 6px 20px rgba(79, 195, 247, 0.3);
}
.control-group {
margin-bottom: 8px;
}
.control-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.control-row label {
font-size: 11px;
color: #ccc;
margin: 0;
width: 70px;
min-width: 70px;
flex-shrink: 0;
}
.control-row input, .control-row select {
flex: 1;
width: 0; /* Important: allows flex to work properly */
min-width: 0;
padding: 3px 6px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
font-size: 11px;
height: 24px;
box-sizing: border-box;
}
.control-row input:focus, .control-row select:focus {
outline: none;
border-color: #4fc3f7;
box-shadow: 0 0 4px rgba(79, 195, 247, 0.3);
}
#fileInput {
width: 100%;
padding: 3px 6px;
border: 1px dashed rgba(79, 195, 247, 0.5);
border-radius: 4px;
background: rgba(79, 195, 247, 0.1);
color: #fff;
font-size: 11px;
cursor: pointer;
margin-bottom: 6px;
height: 24px;
box-sizing: border-box;
}
#fileInput:hover {
border-color: #4fc3f7;
background: rgba(79, 195, 247, 0.2);
}
button {
width: 100%;
background: linear-gradient(145deg, #1976d2, #1565c0);
color: white;
border: none;
padding: 6px 8px;
border-radius: 4px;
cursor: pointer;
font-size: 11px;
font-weight: 500;
transition: all 0.2s ease;
margin-bottom: 3px;
}
button:hover:not(:disabled) {
background: linear-gradient(145deg, #1565c0, #0d47a1);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(21, 101, 192, 0.3);
}
button:disabled {
background: linear-gradient(145deg, #424242, #212121);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
#processButton.highlight {
background: linear-gradient(145deg, #ff9800, #f57c00) !important;
animation: pulse 2s infinite;
box-shadow: 0 0 15px rgba(255, 152, 0, 0.4);
}
@keyframes pulse {
0% { box-shadow: 0 0 15px rgba(255, 152, 0, 0.4); }
50% { box-shadow: 0 0 25px rgba(255, 152, 0, 0.6); }
100% { box-shadow: 0 0 15px rgba(255, 152, 0, 0.4); }
}
#fileInfo, #statsInfo {
font-size: 10px;
color: #999;
padding: 4px 6px;
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
margin-bottom: 6px;
border-left: 3px solid #4fc3f7;
word-break: break-all;
overflow-wrap: break-word;
max-width: 100%;
line-height: 1.3;
}
.filename-truncate {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 180px;
cursor: help;
}
#fileInfo strong, #statsInfo strong {
color: #4fc3f7;
}
#loadingMessage {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(40, 40, 40, 0.9));
backdrop-filter: blur(10px);
padding: 20px 30px;
border-radius: 8px;
border: 1px solid rgba(79, 195, 247, 0.3);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
z-index: 101;
font-size: 14px;
color: #4fc3f7;
text-align: center;
}
#exportDialog {
display: none;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: linear-gradient(145deg, rgba(20, 20, 20, 0.95), rgba(40, 40, 40, 0.9));
backdrop-filter: blur(10px);
border: 1px solid rgba(79, 195, 247, 0.3);
padding: 20px;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
z-index: 102;
width: 280px;
}
#exportDialog h3 {
margin: 0 0 15px 0;
color: #4fc3f7;
font-size: 16px;
text-align: center;
}
#exportDialog input {
width: 100%;
margin-bottom: 15px;
padding: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
color: #fff;
box-sizing: border-box;
}
#exportDialog .buttons {
display: flex;
gap: 10px;
}
#exportDialog .buttons button {
flex: 1;
margin-bottom: 0;
}
#exportFormat {
font-size: 12px;
color: #999;
margin-bottom: 15px;
text-align: center;
}
/* Highlight 6-tuple mode */
.tuple-mode-indicator {
font-size: 10px;
color: #ff9800;
font-weight: bold;
margin-left: 4px;
}
/* Highlight continuous path mode */
.continuous-path-indicator {
font-size: 10px;
color: #4caf50;
font-weight: bold;
margin-left: 4px;
}
/* Highlight hilbert curve mode */
.hilbert-indicator {
font-size: 10px;
color: #00bcd4;
font-weight: bold;
margin-left: 4px;
}
/* Highlight BVH mode */
.bvh-indicator {
font-size: 10px;
color: #ff5722;
font-weight: bold;
margin-left: 4px;
}
/* Highlight lattice 2d mode */
.lattice-indicator {
font-size: 10px;
color: #e91e63;
font-weight: bold;
margin-left: 4px;
}
/* Highlight tiled mode */
.tiled-indicator {
font-size: 10px;
color: #9c27b0;
font-weight: bold;
margin-left: 4px;
}
/* Scrollbar styling for webkit browsers */
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar-thumb {
background: rgba(79, 195, 247, 0.6);
border-radius: 2px;
}
</style>
</head>
<body>
<div id="container"></div>
<div id="dropZone">
<div class="drop-content">
<div class="drop-icon">📊</div>
<div class="drop-text">Drop file here</div>
<div class="drop-subtext">Binary data, images, or URLs</div>
</div>
</div>
<div id="controls">
<h3>
🎯 DataPrism
<button id="minimizeButton" title="Minimize controls"></button>
</h3>
<input type="file" id="fileInput">
<div id="fileInfo"></div>
<div id="statsInfo"></div>
<div class="control-group">
<div class="control-row">
<label for="tupleMode">Mode:</label>
<select id="tupleMode">
<option value="3-tuple">3-Tuple (XYZ)</option>
<option value="6-tuple">6-Tuple (XYZRGB)</option>
</select>
</div>
<div class="control-row">
<label for="dataType">Type:</label>
<select id="dataType">
<option value="int8">Int8</option>
<option value="uint8">Uint8</option>
<option value="int16">Int16</option>
<option value="uint16">Uint16</option>
<option value="int32">Int32</option>
<option value="uint32">Uint32</option>
<option value="fp8_e4m3">FP8 (E4M3)</option>
<option value="fp8_e5m2">FP8 (E5M2)</option>
<option value="fp16">Float16</option>
<option value="bf16">BFloat16</option>
<option value="fp32">Float32</option>
</select>
</div>
<div class="control-row">
<label for="startOffset">Start Offset:</label>
<input type="number" id="startOffset" min="0" step="1" value="0">
</div>
<div class="control-row">
<label for="chunkSize">Chunk (MB):</label>
<input type="number" id="chunkSize" min="0.1" max="100" step="0.1" value="1">
</div>
<div class="control-row">
<label for="gridSize">Grid:</label>
<input type="number" id="gridSize" min="1" max="10" step="1" value="3">
</div>
<div class="control-row">
<label for="spacing">Gap:</label>
<input type="number" id="spacing" min="0.5" max="10" step="0.1" value="2.5">
</div>
<div class="control-row">
<label for="pointSize">Size:</label>
<input type="number" id="pointSize" min="0.005" max="1.5" step="0.01" value="0.005">
</div>
<div class="control-row">
<label for="endianness">Endian:</label>
<select id="endianness">
<option value="true">Little</option>
<option value="false">Big</option>
</select>
</div>
<div class="control-row">
<label for="projectionMode">Projection:</label>
<select id="projectionMode">
<option value="standard">Standard</option>
<option value="continuous-path">Continuous Path</option>
<option value="hilbert-curve">Hilbert Curve</option>
<option value="bvh-with-points">BVH + Points</option>
<option value="bvh-only">BVH Only</option>
<option value="lattice-2d">Lattice 2D</option>
<option value="tiled">Tiled</option>
<option value="stereographic">Stereographic</option>
<option value="equirectangular">Equirectangular</option>
<option value="orthographic-xy">Orthographic XY</option>
<option value="orthographic-xz">Orthographic XZ</option>
<option value="orthographic-yz">Orthographic YZ</option>
<option value="orthographic-3plane">Orthographic 3-Plane</option>
<option value="cylindrical">Cylindrical</option>
</select>
</div>
<div class="control-row" id="bvhControls" style="display: none;">
<label for="bvhMaxDepth">BVH Depth:</label>
<input type="number" id="bvhMaxDepth" min="1" max="12" step="1" value="8">
</div>
<div class="control-row" id="bvhMinPointsControl" style="display: none;">
<label for="bvhMinPoints">Min Points:</label>
<input type="number" id="bvhMinPoints" min="1" max="64" step="1" value="8">
</div>
<div class="control-row" id="bvhLevelControl" style="display: none;">
<label for="bvhDisplayLevel">Show Level:</label>
<select id="bvhDisplayLevel">
<option value="-1">All Levels</option>
<option value="0">Level 0 (Root)</option>
<option value="1">Level 1</option>
<option value="2">Level 2</option>
<option value="3">Level 3</option>
<option value="4">Level 4</option>
<option value="5">Level 5</option>
<option value="6">Level 6</option>
<option value="7">Level 7</option>
<option value="8">Level 8</option>
</select>
</div>
<div class="control-row">
<label for="useQuantization">Quantize:</label>
<input type="checkbox" id="useQuantization" checked style="width: auto; flex: none;">
<span style="font-size: 10px; color: #999; margin-left: 4px;">Remove duplicates</span>
</div>
<div class="control-row">
<label for="quantizationBits">Q-Bits:</label>
<input type="number" id="quantizationBits" min="2" max="10" step="1" value="8">
</div>
</div>
<button id="processButton" disabled>🔄 Process File</button>
<button id="resetButton">🎯 Reset View</button>
<button id="exportPlyButton" disabled>📄 Export PLY</button>
</div>
<button id="restoreButton" title="Show controls">🎯</button>
<div id="loadingMessage">
<div>⏳ Processing data...</div>
<div style="font-size: 12px; margin-top: 8px; opacity: 0.8;">Please wait</div>
</div>
<div id="exportDialog">
<h3>Export File</h3>
<label for="exportFilename" style="display: block; margin-bottom: 8px; font-size: 12px;">Filename:</label>
<input type="text" id="exportFilename" placeholder="Enter filename">
<div id="exportFormat"></div>
<div class="buttons">
<button id="cancelExport">❌ Cancel</button>
<button id="confirmExport">✅ Export</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/18.6.4/tween.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.js"></script>
<script>
// Data type configuration constants
const DATA_TYPES = {
int8: { size: 1, min: -128, max: 127, method: 'getInt8', isFloat: false },
uint8: { size: 1, min: 0, max: 255, method: 'getUint8', isFloat: false },
int16: { size: 2, min: -32768, max: 32767, method: 'getInt16', isFloat: false },
uint16: { size: 2, min: 0, max: 65535, method: 'getUint16', isFloat: false },
int32: { size: 4, min: -2147483648, max: 2147483647, method: 'getInt32', isFloat: false },
uint32: { size: 4, min: 0, max: 4294967295, method: 'getUint32', isFloat: false },
fp16: { size: 2, method: 'getFloat16', isFloat: true },
bf16: { size: 2, method: 'getBFloat16', isFloat: true },
fp32: { size: 4, method: 'getFloat32', isFloat: true },
fp8_e4m3: { size: 1, method: 'getFloat8E4M3', isFloat: true },
fp8_e5m2: { size: 1, method: 'getFloat8E5M2', isFloat: true }
};
// Pre-calculate normalization multipliers for integer types
const NORMALIZERS = Object.fromEntries(
Object.entries(DATA_TYPES)
.filter(([type, config]) => !config.isFloat)
.map(([type, config]) => [
type,
{
multiplier: 2 / (config.max - config.min),
offset: config.min
}
])
);
/**
* BVH (Bounding Volume Hierarchy) Implementation
* Creates a binary tree of axis-aligned bounding boxes
*/
const BVH = {
/**
* Build a BVH tree from points
* @param {Float32Array} points - Array of 3D coordinates
* @param {number} maxDepth - Maximum tree depth
* @param {number} minPoints - Minimum points per leaf node
* @returns {Object} Root node of BVH tree
*/
build: function(points, maxDepth = 8, minPoints = 8) {
try {
const numPoints = points.length / 3;
const indices = new Array(numPoints);
for (let i = 0; i < numPoints; i++) indices[i] = i;
console.log(`Building BVH with ${numPoints} points, maxDepth=${maxDepth}, minPoints=${minPoints}`);
return this.buildNode(points, indices, 0, maxDepth, minPoints);
} catch (error) {
console.error('Error in BVH.build:', error);
console.error('Error stack:', error.stack);
throw error;
}
},
/**
* Recursively build BVH node
*/
buildNode: function(points, indices, depth, maxDepth, minPoints) {
// Calculate AABB for this node
const bounds = this.calculateAABB(points, indices);
const node = {
bounds: bounds,
indices: indices,
depth: depth,
left: null,
right: null,
isLeaf: false
};
// Check termination conditions
if (depth >= maxDepth || indices.length <= minPoints) {
node.isLeaf = true;
return node;
}
// Find longest axis and split
const size = {
x: bounds.max.x - bounds.min.x,
y: bounds.max.y - bounds.min.y,
z: bounds.max.z - bounds.min.z
};
let axis = 0; // 0=x, 1=y, 2=z
let maxSize = size.x;
if (size.y > maxSize) { axis = 1; maxSize = size.y; }
if (size.z > maxSize) { axis = 2; maxSize = size.z; }
// Sort indices along chosen axis
indices.sort((a, b) => {
const aVal = points[a * 3 + axis];
const bVal = points[b * 3 + axis];
return aVal - bVal;
});
// Split at median
const mid = Math.floor(indices.length / 2);
const leftIndices = indices.slice(0, mid);
const rightIndices = indices.slice(mid);
// Recursively build children
if (leftIndices.length > 0) {
node.left = this.buildNode(points, leftIndices, depth + 1, maxDepth, minPoints);
}
if (rightIndices.length > 0) {
node.right = this.buildNode(points, rightIndices, depth + 1, maxDepth, minPoints);
}
return node;
},
/**
* Calculate axis-aligned bounding box for point indices
*/
calculateAABB: function(points, indices) {
const bounds = {
min: { x: Infinity, y: Infinity, z: Infinity },
max: { x: -Infinity, y: -Infinity, z: -Infinity }
};
for (const idx of indices) {
const i = idx * 3;
const x = points[i];
const y = points[i + 1];
const z = points[i + 2];
bounds.min.x = Math.min(bounds.min.x, x);
bounds.min.y = Math.min(bounds.min.y, y);
bounds.min.z = Math.min(bounds.min.z, z);
bounds.max.x = Math.max(bounds.max.x, x);
bounds.max.y = Math.max(bounds.max.y, y);
bounds.max.z = Math.max(bounds.max.z, z);
}
return bounds;
},
/**
* Count total nodes in tree (for instancing)
*/
countNodes: function(node) {
if (!node) return 0;
return 1 + this.countNodes(node.left) + this.countNodes(node.right);
},
/**
* Flatten tree into arrays for instanced rendering
*/
flattenTree: function(node, centers, sizes, colors, maxDepth, displayLevel = -1) {
const nodes = [];
const traverse = (n) => {
if (!n) return;
// Skip if filtering by specific level
if (displayLevel >= 0 && n.depth !== displayLevel) {
traverse(n.left);
traverse(n.right);
return;
}
// Calculate center and size
const center = new THREE.Vector3(
(n.bounds.min.x + n.bounds.max.x) / 2,
(n.bounds.min.y + n.bounds.max.y) / 2,
(n.bounds.min.z + n.bounds.max.z) / 2
);
const size = new THREE.Vector3(
n.bounds.max.x - n.bounds.min.x,
n.bounds.max.y - n.bounds.min.y,
n.bounds.max.z - n.bounds.min.z
);
// Color based on depth (rainbow gradient)
const t = n.depth / Math.max(1, maxDepth);
const color = new THREE.Color();
color.setHSL(t * 0.7, 0.8, 0.5); // 0 to 0.7 goes from red to blue
nodes.push({ center, size, color });
traverse(n.left);
traverse(n.right);
};
traverse(node);
return nodes;
}
};
/**
* Create instanced wireframe boxes for BVH visualization
* Uses custom shader to apply per-instance transforms
*/
function createInstancedBVHBoxes(bvhNodes) {
try {
const numBoxes = bvhNodes.length;
if (numBoxes === 0) {
console.warn('No BVH nodes to render');
return null;
}
console.log(`Creating instanced BVH boxes for ${numBoxes} nodes`);
// Create unit cube edge geometry (12 edges)
const edges = [
// Bottom face
[-0.5, -0.5, -0.5], [0.5, -0.5, -0.5],
[0.5, -0.5, -0.5], [0.5, -0.5, 0.5],
[0.5, -0.5, 0.5], [-0.5, -0.5, 0.5],
[-0.5, -0.5, 0.5], [-0.5, -0.5, -0.5],
// Top face
[-0.5, 0.5, -0.5], [0.5, 0.5, -0.5],
[0.5, 0.5, -0.5], [0.5, 0.5, 0.5],
[0.5, 0.5, 0.5], [-0.5, 0.5, 0.5],
[-0.5, 0.5, 0.5], [-0.5, 0.5, -0.5],
// Vertical edges
[-0.5, -0.5, -0.5], [-0.5, 0.5, -0.5],
[0.5, -0.5, -0.5], [0.5, 0.5, -0.5],
[0.5, -0.5, 0.5], [0.5, 0.5, 0.5],
[-0.5, -0.5, 0.5], [-0.5, 0.5, 0.5]
];
// Flatten edge vertices
const positions = new Float32Array(edges.length * 3);
for (let i = 0; i < edges.length; i++) {
positions[i * 3] = edges[i][0];
positions[i * 3 + 1] = edges[i][1];
positions[i * 3 + 2] = edges[i][2];
}
// Create instanced buffer geometry
const geometry = new THREE.InstancedBufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
// Create instance attributes (one per box)
const instanceCenters = new Float32Array(numBoxes * 3);
const instanceSizes = new Float32Array(numBoxes * 3);
const instanceColors = new Float32Array(numBoxes * 3);
for (let i = 0; i < numBoxes; i++) {
const node = bvhNodes[i];
if (!node || !node.center || !node.size || !node.color) {
console.error(`Invalid BVH node at index ${i}:`, node);
throw new Error(`Invalid BVH node at index ${i}`);
}
// Center
instanceCenters[i * 3] = node.center.x;
instanceCenters[i * 3 + 1] = node.center.y;
instanceCenters[i * 3 + 2] = node.center.z;
// Size (half-extents become full extents for scaling)
instanceSizes[i * 3] = node.size.x;
instanceSizes[i * 3 + 1] = node.size.y;
instanceSizes[i * 3 + 2] = node.size.z;
// Color
instanceColors[i * 3] = node.color.r;
instanceColors[i * 3 + 1] = node.color.g;
instanceColors[i * 3 + 2] = node.color.b;
}
geometry.setAttribute('instanceCenter', new THREE.InstancedBufferAttribute(instanceCenters, 3));
geometry.setAttribute('instanceSize', new THREE.InstancedBufferAttribute(instanceSizes, 3));
geometry.setAttribute('instanceColor', new THREE.InstancedBufferAttribute(instanceColors, 3));
// Custom shader material for instanced lines
const material = new THREE.ShaderMaterial({
vertexShader: `
attribute vec3 instanceCenter;
attribute vec3 instanceSize;
attribute vec3 instanceColor;
varying vec3 vColor;
void main() {
vColor = instanceColor;
// Apply instance transform: scale then translate
vec3 transformed = position * instanceSize + instanceCenter;
gl_Position = projectionMatrix * modelViewMatrix * vec4(transformed, 1.0);
}
`,
fragmentShader: `
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}
`,
transparent: false,
depthTest: true,
depthWrite: true
});
// Create line segments
const lineSegments = new THREE.LineSegments(geometry, material);
console.log(`Successfully created BVH line segments with ${numBoxes} instances`);
return lineSegments;
} catch (error) {
console.error('Error in createInstancedBVHBoxes:', error);
console.error('Error stack:', error.stack);
throw error;
}
}
/**
* 3D Hilbert curve implementation
* Maps between 3D coordinates and 1D position along the curve
*/
const HilbertCurve3D = {
// Rotation matrices for the 8 octants of the Hilbert curve
rotations: [
[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0],
[2, 0, 1], [2, 1, 0], [0, 1, 2], [0, 2, 1]
],
// Convert 3D coordinates to Hilbert index
coordsToIndex: function(x, y, z, order) {
let index = 0;
for (let i = order - 1; i >= 0; i--) {
const bits = [
(x >> i) & 1,
(y >> i) & 1,
(z >> i) & 1
];
// Calculate octant
const octant = bits[0] * 4 + bits[1] * 2 + bits[2];
index = (index << 3) | octant;
// Apply rotation for next level
const temp = [bits[0], bits[1], bits[2]];
const rot = this.rotations[octant];
bits[0] = temp[rot[0]];
bits[1] = temp[rot[1]];
bits[2] = temp[rot[2]];
}
return index;
},
// Convert Hilbert index to 3D coordinates
indexToCoords: function(index, order) {
let x = 0, y = 0, z = 0;
let octant = 0;
for (let i = 0; i < order; i++) {
octant = index & 7;
index >>= 3;
const rot = this.rotations[octant];
const bits = [
(octant >> 2) & 1,
(octant >> 1) & 1,
octant & 1
];
// Apply inverse rotation
x = (x << 1) | bits[rot[0]];
y = (y << 1) | bits[rot[1]];
z = (z << 1) | bits[rot[2]];
}
return [x, y, z];
}
};
/**
* Apply various 3D to 2D projections to reveal geometric patterns
* @param {Float32Array} points - Array of 3D coordinates [x1,y1,z1,x2,y2,z2,...]
* @param {string} projectionMode - Type of projection to apply
* @param {number} quantizationBits - Number of quantization bits (for tiled projection)
* @returns {Float32Array|Object} - Projected points or object with points and path data
*/
function applyProjection(points, projectionMode, quantizationBits = 8) {
if (projectionMode === 'standard') {
return points; // No projection
}
// BVH visualization modes
if (projectionMode === 'bvh-with-points' || projectionMode === 'bvh-only') {
try {
const numPoints = points.length / 3;
console.log(`Starting BVH projection mode: ${projectionMode}, numPoints: ${numPoints}`);
// Get BVH parameters from UI
const maxDepthElement = document.getElementById('bvhMaxDepth');
const minPointsElement = document.getElementById('bvhMinPoints');
const displayLevelElement = document.getElementById('bvhDisplayLevel');
const maxDepth = maxDepthElement ? parseInt(maxDepthElement.value) : 8;
const minPoints = minPointsElement ? parseInt(minPointsElement.value) : 8;
const displayLevel = displayLevelElement ? parseInt(displayLevelElement.value) : -1;
console.log(`BVH parameters: maxDepth=${maxDepth}, minPoints=${minPoints}, displayLevel=${displayLevel}`);
// Validate parameters
if (isNaN(maxDepth) || maxDepth < 1 || maxDepth > 12) {
throw new Error(`Invalid maxDepth: ${maxDepth}`);
}
if (isNaN(minPoints) || minPoints < 1) {
throw new Error(`Invalid minPoints: ${minPoints}`);
}
if (numPoints === 0) {
throw new Error('No points to build BVH');
}
// Build BVH tree
console.log('Building BVH tree...');
const bvhRoot = BVH.build(points, maxDepth, minPoints);
console.log('BVH tree built successfully');
// Flatten tree for rendering
console.log('Flattening BVH tree...');
const bvhNodes = BVH.flattenTree(bvhRoot, null, null, null, maxDepth, displayLevel);
console.log(`BVH flattened: ${bvhNodes.length} boxes at ${displayLevel >= 0 ? 'level ' + displayLevel : 'all levels'}`);
if (bvhNodes.length === 0) {
console.warn('No BVH nodes generated - returning standard points');
return points; // Just return points as-is for standard handling
}
// Return special structure indicating BVH mode
// We return the points as-is and attach bvhData which will be handled specially
return {
points: points, // Keep original points
bvhData: {
nodes: bvhNodes,
showPoints: projectionMode === 'bvh-with-points'
}
};
} catch (error) {
console.error('Error in BVH projection mode:', error);
console.error('Error stack:', error.stack);
console.error('Error details:', {
message: error.message,
name: error.name,
projectionMode: projectionMode,
pointsLength: points.length
});
// Return standard points as fallback
return points;
}
}
// Special case for continuous path - return both points and path data
if (projectionMode === 'continuous-path') {
return {
points: points,
pathData: true, // Flag to indicate this chunk should create path lines
numPoints: points.length / 3
};
}
// Hilbert curve projection - rearrange points along a 3D Hilbert curve
if (projectionMode === 'hilbert-curve') {
const numPoints = points.length / 3;
const order = Math.max(2, Math.min(8, Math.ceil(Math.log2(Math.cbrt(numPoints))))); // Adaptive order
const gridSize = Math.pow(2, order);
// Create array to store points with their Hilbert indices
const indexedPoints = [];
for (let i = 0; i < numPoints; i++) {
const pointIndex = i * 3;
const x = points[pointIndex];
const y = points[pointIndex + 1];
const z = points[pointIndex + 2];
// Convert normalized coordinates [-1,1] to discrete grid [0, gridSize-1]
const discreteX = Math.max(0, Math.min(gridSize - 1, Math.floor((x + 1) / 2 * gridSize)));
const discreteY = Math.max(0, Math.min(gridSize - 1, Math.floor((y + 1) / 2 * gridSize)));
const discreteZ = Math.max(0, Math.min(gridSize - 1, Math.floor((z + 1) / 2 * gridSize)));
// Calculate Hilbert index for this point
const hilbertIndex = HilbertCurve3D.coordsToIndex(discreteX, discreteY, discreteZ, order);
indexedPoints.push({
hilbertIndex: hilbertIndex,
originalIndex: i,
x: x,
y: y,
z: z
});
}
// Sort points by Hilbert index to create a continuous path
indexedPoints.sort((a, b) => a.hilbertIndex - b.hilbertIndex);
// Create new point array in Hilbert order
const orderedPoints = new Float32Array(points.length);
for (let i = 0; i < indexedPoints.length; i++) {
const pt = indexedPoints[i];
orderedPoints[i * 3] = pt.x;
orderedPoints[i * 3 + 1] = pt.y;
orderedPoints[i * 3 + 2] = pt.z;
}
// Return with path data flag to draw connecting lines
return {
points: orderedPoints,
pathData: true,
numPoints: numPoints
};
}
// New Lattice 2D projection case
if (projectionMode === 'lattice-2d') {
const numPoints = points.length / 3;
const latticeSize = Math.ceil(Math.sqrt(numPoints));
// Create new projected points array
const projectedPoints = new Float32Array(points.length);
for (let i = 0; i < numPoints; i++) {
const pointIndex = i * 3;
// Calculate lattice position
const row = Math.floor(i / latticeSize);
const col = i % latticeSize;
// Map to normalized coordinates [-1, 1] with proper spacing
const x = latticeSize > 1 ? (col / (latticeSize - 1)) * 2 - 1 : 0;
const y = latticeSize > 1 ? (row / (latticeSize - 1)) * 2 - 1 : 0;
projectedPoints[pointIndex] = x;
projectedPoints[pointIndex + 1] = y;
projectedPoints[pointIndex + 2] = 0; // Flatten to z=0 plane
}
return projectedPoints;
}
// New Tiled projection case
if (projectionMode === 'tiled') {
const q = quantizationBits;
const qRange = Math.pow(2, q);
const sqrtQRange = Math.floor(Math.sqrt(qRange));
// Create new projected points array
const projectedPoints = new Float32Array(points.length);
for (let i = 0; i < points.length; i += 3) {
const x = points[i];
const y = points[i + 1];
const z = points[i + 2];
// Convert normalized coordinates [-1,1] to discrete grid [0, qRange-1]
const discreteX = Math.max(0, Math.min(qRange - 1, Math.floor((x + 1) / 2 * qRange)));
const discreteY = Math.max(0, Math.min(qRange - 1, Math.floor((y + 1) / 2 * qRange)));
const discreteZ = Math.max(0, Math.min(qRange - 1, Math.floor((z + 1) / 2 * qRange)));
// Apply tiling formula: (col, row) = (z % sqrt(2^q), floor(z / sqrt(2^q)))
const col = discreteZ % sqrtQRange;
const row = Math.floor(discreteZ / sqrtQRange);
// Calculate tiled coordinates: col * 2^q + x, row * 2^q + y
const tiledX = col * qRange + discreteX;
const tiledY = row * qRange + discreteY;
// Normalize back for display (scale to fit in reasonable viewing area)
const maxTiledCoord = Math.max(sqrtQRange * qRange + qRange - 1, 1);
projectedPoints[i] = (tiledX / maxTiledCoord) * 2 - 1;
projectedPoints[i + 1] = (tiledY / maxTiledCoord) * 2 - 1;
projectedPoints[i + 2] = 0; // Flatten to z=0 plane
}
return projectedPoints;
}
// Special case for 3-plane orthographic - creates 3x more points
if (projectionMode === 'orthographic-3plane') {
const projectedPoints = new Float32Array(points.length * 3); // Triple the size
for (let i = 0; i < points.length; i += 3) {
const x = points[i];
const y = points[i + 1];
const z = points[i + 2];
// Calculate base index for the three projected points
const baseIdx = i * 3;
// Project onto XY plane (z = 0)
projectedPoints[baseIdx] = x;
projectedPoints[baseIdx + 1] = y;
projectedPoints[baseIdx + 2] = 0;
// Project onto XZ plane (y = 0)
projectedPoints[baseIdx + 3] = x;
projectedPoints[baseIdx + 4] = 0;
projectedPoints[baseIdx + 5] = z;
// Project onto YZ plane (x = 0)
projectedPoints[baseIdx + 6] = 0;
projectedPoints[baseIdx + 7] = y;
projectedPoints[baseIdx + 8] = z;
}
return projectedPoints;
}
// Standard single-point projections
const projectedPoints = new Float32Array(points.length);
for (let i = 0; i < points.length; i += 3) {
let x = points[i];
let y = points[i + 1];
let z = points[i + 2];
switch (projectionMode) {
case 'stereographic':
// Normalize to unit sphere
const magnitude = Math.sqrt(x * x + y * y + z * z);
if (magnitude > 0) {
x /= magnitude;
y /= magnitude;
z /= magnitude;
}
// Stereographic projection from north pole (0,0,1) to z=0 plane
if (z < 0.999) { // Avoid division by zero
const denominator = 1 - z;
const projX = x / denominator;
const projY = y / denominator;
// Scale down the projection for better visualization
const scale = 0.5;
projectedPoints[i] = projX * scale;
projectedPoints[i + 1] = projY * scale;
projectedPoints[i + 2] = 0; // Flatten to z=0 plane
} else {
// Point very close to north pole, place at origin
projectedPoints[i] = 0;
projectedPoints[i + 1] = 0;
projectedPoints[i + 2] = 0;
}
break;
case 'equirectangular':
// Convert cartesian to spherical coordinates
const r = Math.sqrt(x * x + y * y + z * z);
if (r > 0) {
// Spherical coordinates: theta (azimuth), phi (elevation)
const theta = Math.atan2(y, x); // azimuth [-π, π]
const phi = Math.acos(Math.abs(z) / r); // elevation [0, π]
// Map to equirectangular coordinates
// Longitude: theta mapped to [-1, 1]
// Latitude: phi mapped to [-1, 1]
projectedPoints[i] = theta / Math.PI; // maps [-π,π] to [-1,1]
projectedPoints[i + 1] = (phi / Math.PI) * 2 - 1; // maps [0,π] to [-1,1]
projectedPoints[i + 2] = 0; // Flatten to z=0 plane
} else {
projectedPoints[i] = 0;
projectedPoints[i + 1] = 0;
projectedPoints[i + 2] = 0;
}
break;
case 'orthographic-xy':
// Project onto XY plane (view from Z axis)
projectedPoints[i] = x;
projectedPoints[i + 1] = y;
projectedPoints[i + 2] = 0;
break;
case 'orthographic-xz':
// Project onto XZ plane (view from Y axis)
projectedPoints[i] = x;
projectedPoints[i + 1] = z;
projectedPoints[i + 2] = 0;
break;
case 'orthographic-yz':
// Project onto YZ plane (view from X axis)
projectedPoints[i] = y;
projectedPoints[i + 1] = z;
projectedPoints[i + 2] = 0;
break;
case 'cylindrical':
// Cylindrical projection: wrap around Y axis
const radius = Math.sqrt(x * x + z * z);
if (radius > 0) {
// Azimuth angle around Y axis
const angle = Math.atan2(z, x); // [-π, π]
// Map to cylindrical coordinates
projectedPoints[i] = angle / Math.PI; // maps [-π,π] to [-1,1]
projectedPoints[i + 1] = y; // height remains the same
projectedPoints[i + 2] = 0; // Flatten to z=0 plane
} else {
projectedPoints[i] = 0;
projectedPoints[i + 1] = y;
projectedPoints[i + 2] = 0;
}
break;
default:
// Fallback to standard (no projection)
projectedPoints[i] = x;
projectedPoints[i + 1] = y;
projectedPoints[i + 2] = z;
break;
}
}
return projectedPoints;
}
/**
* Convert IEEE 754 half-precision (fp16) to single precision (fp32)
* @param {number} uint16Value - 16-bit unsigned integer representing fp16
* @returns {number} - JavaScript number (fp32/fp64)
*/
function fp16ToFloat32(uint16Value) {
const sign = (uint16Value & 0x8000) >> 15;
const exponent = (uint16Value & 0x7C00) >> 10;
const mantissa = uint16Value & 0x03FF;
if (exponent === 0) {
if (mantissa === 0) {
// Zero
return sign === 0 ? 0.0 : -0.0;
} else {
// Subnormal
return (sign === 0 ? 1 : -1) * Math.pow(2, -14) * (mantissa / 1024);
}
} else if (exponent === 31) {
if (mantissa === 0) {
// Infinity
return sign === 0 ? Infinity : -Infinity;
} else {
// NaN
return NaN;
}
} else {
// Normal
return (sign === 0 ? 1 : -1) * Math.pow(2, exponent - 15) * (1 + mantissa / 1024);
}
}
/**
* Convert Google's bfloat16 (bf16) to single precision (fp32)
* @param {number} uint16Value - 16-bit unsigned integer representing bf16
* @returns {number} - JavaScript number (fp32/fp64)
*/
function bf16ToFloat32(uint16Value) {
const sign = (uint16Value & 0x8000) >> 15;
const exponent = (uint16Value & 0x7F80) >> 7; // bits 14-7 (8 bits)
const mantissa = uint16Value & 0x007F; // bits 6-0 (7 bits)
if (exponent === 0) {
if (mantissa === 0) {
// Zero
return sign === 0 ? 0.0 : -0.0;
} else {
// Subnormal
return (sign === 0 ? 1 : -1) * Math.pow(2, -126) * (mantissa / 128);
}
} else if (exponent === 255) {
if (mantissa === 0) {
// Infinity
return sign === 0 ? Infinity : -Infinity;
} else {
// NaN
return NaN;
}
} else {
// Normal
return (sign === 0 ? 1 : -1) * Math.pow(2, exponent - 127) * (1 + mantissa / 128);
}
}
function fp8e4m3ToFloat32(uint8Value) {
const sign = (uint8Value & 0x80) >> 7;
const exponent = (uint8Value & 0x78) >> 3; // 4 bits
const mantissa = uint8Value & 0x07; // 3 bits
if (exponent === 0) {
// Subnormal or zero
if (mantissa === 0) return sign ? -0.0 : 0.0;
return (sign ? -1 : 1) * Math.pow(2, -6) * (mantissa / 8);
}
if (exponent === 0xF) {
// Inf or NaN
return mantissa === 0 ? (sign ? -Infinity : Infinity) : NaN;
}
return (sign ? -1 : 1) * Math.pow(2, exponent - 7) * (1 + mantissa / 8);
}
function fp8e5m2ToFloat32(uint8Value) {
const sign = (uint8Value & 0x80) >> 7;
const exponent = (uint8Value & 0x7C) >> 2; // 5 bits
const mantissa = uint8Value & 0x03; // 2 bits
if (exponent === 0) {
if (mantissa === 0) return sign ? -0.0 : 0.0;
return (sign ? -1 : 1) * Math.pow(2, -14) * (mantissa / 4);
}
if (exponent === 0x1F) {
return mantissa === 0 ? (sign ? -Infinity : Infinity) : NaN;
}
return (sign ? -1 : 1) * Math.pow(2, exponent - 15) * (1 + mantissa / 4);
}
/**
* Extended DataView with fp16 and bf16 support
*/
function createExtendedDataView(buffer) {
const view = new DataView(buffer);
// Add fp16 support
view.getFloat16 = function(byteOffset, littleEndian = false) {
const uint16Value = this.getUint16(byteOffset, littleEndian);
return fp16ToFloat32(uint16Value);
};
// Add bf16 support
view.getBFloat16 = function(byteOffset, littleEndian = false) {
const uint16Value = this.getUint16(byteOffset, littleEndian);
return bf16ToFloat32(uint16Value);
};
view.getFloat8E4M3 = function(byteOffset) {
const uint8Value = this.getUint8(byteOffset);
return fp8e4m3ToFloat32(uint8Value);
};
view.getFloat8E5M2 = function(byteOffset) {
const uint8Value = this.getUint8(byteOffset);
return fp8e5m2ToFloat32(uint8Value);
};
return view;
}
/**
* Process binary data into quantized normalized 3D points with colors (removes duplicates)
* @param {ArrayBuffer} buffer - The input binary data
* @param {string} dataType - The data type to interpret the buffer as
* @param {boolean} isLittleEndian - Whether to read as little endian
* @param {number} quantizationBits - Number of bits for quantization (2-10)
* @param {string} projectionMode - Projection mode ('standard' or 'stereographic')
* @param {string} tupleMode - Tuple mode ('3-tuple' or '6-tuple')
* @returns {{ points: Float32Array, colors: Float32Array, numPoints: number, pathData?: boolean }}
*/
function quantizeProcessDataAs(buffer, dataType, isLittleEndian, quantizationBits, projectionMode = 'standard', tupleMode = '3-tuple') {
// Input validation
if (!buffer || !(buffer instanceof ArrayBuffer)) {
throw new Error('Invalid buffer provided - must be an ArrayBuffer');
}
const config = DATA_TYPES[dataType];
if (!config) {
throw new Error(`Unsupported data type: ${dataType}. Supported types: ${Object.keys(DATA_TYPES).join(', ')}`);
}
// Validate quantization bits
if (quantizationBits < 2 || quantizationBits > 10) {
throw new Error('Quantization bits must be between 2 and 10');
}
const typeSize = config.size;
const valuesPerTuple = tupleMode === '6-tuple' ? 6 : 3;
const tupleSize = typeSize * valuesPerTuple;
if (buffer.byteLength < tupleSize) {
throw new Error(`Buffer too small for data type ${dataType} in ${tupleMode} mode. Need at least ${tupleSize} bytes, got ${buffer.byteLength}`);
}
const view = createExtendedDataView(buffer);
const maxOffset = buffer.byteLength - tupleSize;
const maxTuples = Math.floor(buffer.byteLength / tupleSize);
// Pre-allocate typed arrays for better performance
let points = new Float32Array(maxTuples * 3);
const colors = new Float32Array(maxTuples * 3);
// Setup normalization function based on data type
const readMethod = view[config.method].bind(view);
let normalize;
if (config.isFloat) {
// Use tanh for floating point normalization
normalize = value => {
// Handle special values
if (!isFinite(value)) {
return isNaN(value) ? 0 : (value > 0 ? 1 : -1);
}
// Apply tanh for smooth [-1, 1] mapping
return Math.tanh(value);
};
} else {
// Use linear normalization for integers
const { multiplier, offset } = NORMALIZERS[dataType];
normalize = value => ((value - offset) * multiplier) - 1;
}
let pointIndex = 0;
let baseOffset = 0;
// Calculate quantization parameters based on bit size
const qRange = Math.pow(2, quantizationBits);
const qHalfRange = qRange / 2;
const qMaxIndex = qRange - 1;
// For 6-tuple mode, we need to quantize both coordinates and colors
// But we only deduplicate based on coordinates to preserve color variation
const totalQuantizedPositions = qRange * qRange * qRange;
const bitArraySizeInUint32 = Math.ceil(totalQuantizedPositions / 32);
const tupleBitArray = new Uint32Array(bitArraySizeInUint32);
// Calculate bit shifts for index generation based on quantization bits
const yShift = quantizationBits;
const zShift = quantizationBits * 2;
console.log(`Using ${quantizationBits}-bit quantization in ${tupleMode} mode: ${qRange}³ = ${totalQuantizedPositions.toLocaleString()} possible positions`);
try {
while (baseOffset <= maxOffset) {
// Read and normalize coordinates
const x = normalize(readMethod(baseOffset, isLittleEndian));
const y = normalize(readMethod(baseOffset + typeSize, isLittleEndian));
const z = normalize(readMethod(baseOffset + typeSize * 2, isLittleEndian));
// Quantize coordinates: map [-1,1] to [0,qMaxIndex] with bounds checking
const qx = Math.max(0, Math.min(qMaxIndex, Math.floor((x + 1) * qHalfRange)));
const qy = Math.max(0, Math.min(qMaxIndex, Math.floor((y + 1) * qHalfRange)));
const qz = Math.max(0, Math.min(qMaxIndex, Math.floor((z + 1) * qHalfRange)));
// Create unique index for this quantized position using variable bit shifts
const qIndex = (qz << zShift) | (qy << yShift) | qx;
// Check if we've seen this quantized position before
const elementIndex = qIndex >> 5;
const bitPosition = qIndex & 0x1F;
const mask = 1 << bitPosition;
if ((tupleBitArray[elementIndex] & mask) === 0) {
// Mark this position as seen
tupleBitArray[elementIndex] |= mask;
// Store points (original normalized coordinates, not quantized)
points[pointIndex] = x;
points[pointIndex + 1] = y;
points[pointIndex + 2] = z;
// Handle colors based on tuple mode
if (tupleMode === '6-tuple') {
// Read explicit color values and normalize to [-1,1] then convert to [0,1]
const r = normalize(readMethod(baseOffset + typeSize * 3, isLittleEndian));
const g = normalize(readMethod(baseOffset + typeSize * 4, isLittleEndian));
const b = normalize(readMethod(baseOffset + typeSize * 5, isLittleEndian));
// Convert from [-1,1] to [0,1] for Three.js rendering
colors[pointIndex] = (r + 1) / 2;
colors[pointIndex + 1] = (g + 1) / 2;
colors[pointIndex + 2] = (b + 1) / 2;
} else {
// Generate colors from coordinates (map from [-1,1] to [0,1] for Three.js)
colors[pointIndex] = (x + 1) / 2;
colors[pointIndex + 1] = (y + 1) / 2;
colors[pointIndex + 2] = (z + 1) / 2;
}
pointIndex += 3;
}
baseOffset += tupleSize;
}
} catch (e) {
console.error(`Error processing data at offset: ${baseOffset}`, e);
// Return what we've processed so far rather than failing completely
}
// Apply projection if requested
if (projectionMode !== 'standard') {
const projectionResult = applyProjection(points.slice(0, pointIndex), projectionMode, quantizationBits);
// Handle different projection return types
if (projectionResult && projectionResult.pathData) {
// Continuous path mode - return original points with path flag
points = projectionResult.points.slice(0, pointIndex);
return {
points,
colors: colors.slice(0, pointIndex),
numPoints: pointIndex / 3,
pathData: true
};
} else if (projectionResult && projectionResult.bvhData) {
// BVH mode - return points, colors, and BVH data
points = projectionResult.points;
return {
points,
colors: colors.slice(0, pointIndex),
numPoints: projectionResult.bvhData.showPoints ? (pointIndex / 3) : 0,
bvhNodes: projectionResult.bvhData.nodes,
bvhMode: true,
showPoints: projectionResult.bvhData.showPoints
};
} else if (projectionMode === 'orthographic-3plane') {
// Handle color expansion for 3-plane orthographic projection
const expandedColors = new Float32Array(colors.length * 3);
for (let i = 0; i < pointIndex; i += 3) {
const baseIdx = i * 3;
// Copy original colors for all three projected points
// XY plane projection
expandedColors[baseIdx] = colors[i];
expandedColors[baseIdx + 1] = colors[i + 1];
expandedColors[baseIdx + 2] = colors[i + 2];
// XZ plane projection
expandedColors[baseIdx + 3] = colors[i];
expandedColors[baseIdx + 4] = colors[i + 1];
expandedColors[baseIdx + 5] = colors[i + 2];
// YZ plane projection
expandedColors[baseIdx + 6] = colors[i];
expandedColors[baseIdx + 7] = colors[i + 1];
expandedColors[baseIdx + 8] = colors[i + 2];
}
return {
points: projectionResult,
colors: expandedColors,
numPoints: (pointIndex / 3) * 3
};
} else {
// Standard projection
points = projectionResult;
}
} else {
points = points.slice(0, pointIndex);
}
return {
points,
colors: colors.slice(0, pointIndex),
numPoints: pointIndex / 3
};
}
// Main application
class BinaryPointCloudViewer {
constructor() {
this.fileBuffer = null;
this.scene = null;
this.camera = null;
this.renderer = null;
this.controls = null;
this.pointClouds = [];
this.pathLines = []; // Store path lines separately
this.originalFileName = '';
this.cameraRadius = 5;
this.cameraAngle = 0;
this.totalPoints = 0;
this.controlsMinimized = false;
this.contextLost = false; // Track WebGL context state
this.init();
this.setupEventListeners();
this.setupDropZone();
this.setupPasteHandling();
this.setupVisibilityHandling();
this.checkURLParameters();
this.animate();
}
// Handle clipboard operations
setupPasteHandling() {
document.addEventListener('paste', (e) => {
e.preventDefault();
const items = (e.clipboardData || window.clipboardData).items;
for (let item of items) {
if (item.kind === 'file') {
const file = item.getAsFile();
this.handleDragDropFiles([file]);
} else {
item.getAsString((text) => {
this.tryFetchAPI(text);
});
}
}
});
}
// Try to fetch from URL
async tryFetchAPI(src) {
try {
console.log('Attempting to fetch from URL:', src);
const response = await fetch(src);
if (response.ok) {
const blob = await response.blob();
// Create a fake file from the blob
const file = new File([blob], src.split('/').pop() || 'fetched_file', { type: blob.type });
this.handleDragDropFiles([file]);
} else {
console.log('Failed to fetch URL, response not ok:', response.status);
}
} catch (error) {
console.log('Failed to fetch URL:', error.message);
}
}
// Handle non-file drops (like HTML content with image sources)
handleNonFileDrop(text) {
if (text.startsWith('<meta')) {
try {
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'text/html');
const img = doc.querySelector('img');
if (img) {
const imgSrc = img.getAttribute('src');
this.tryFetchAPI(imgSrc);
}
} catch (error) {
console.log('Failed to parse dropped HTML content:', error.message);
}
} else if (text.startsWith('http')) {
// Direct URL
this.tryFetchAPI(text);
}
}
// Handle drop events
handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files && files.length > 0) {
this.handleDragDropFiles(files);
return;
}
// Handle non-file drops
for (let i = 0; i < dt.items.length; i++) {
const item = dt.items[i];
if (item.kind === 'string') {
item.getAsString((s) => {
this.handleNonFileDrop(s);
});
}
}
}
// Setup drag and drop functionality
setupDropZone() {
const dropZone = document.getElementById('dropZone');
// Prevent default drag behaviors on the document
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
document.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
}, false);
});
// Show drop zone when dragging over document
document.addEventListener('dragenter', (e) => {
dropZone.style.display = 'flex';
dropZone.classList.remove('dragover');
});
// Hide drop zone when leaving the drop zone itself
dropZone.addEventListener('dragleave', (e) => {
// Only hide if we're leaving the dropZone entirely, not just moving between child elements
if (!dropZone.contains(e.relatedTarget)) {
dropZone.style.display = 'none';
dropZone.classList.remove('dragover');
}
});
// Highlight drop zone when dragging over it
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('dragover');
});
// Handle the actual drop
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.display = 'none';
dropZone.classList.remove('dragover');
this.handleDrop(e);
});
}
// Handle files (from drag and drop or file input)
handleFiles(files) {
if (!files || files.length === 0) return;
const file = files[0]; // Take the first file
this.originalFileName = file.name;
const fileInfoDiv = document.getElementById('fileInfo');
fileInfoDiv.innerHTML = `
<strong>File:</strong> <span class="filename-truncate" title="${file.name}">${file.name}</span> <strong>Size:</strong> ${(file.size / (1024 * 1024)).toFixed(2)} MB
`;
const reader = new FileReader();
reader.onload = (e) => {
this.fileBuffer = e.target.result;
document.getElementById('processButton').disabled = false;
document.getElementById('exportPlyButton').disabled = true;
this.highlightProcessButton(); // Highlight when file is loaded
console.log('File loaded successfully:', file.name);
};
reader.onerror = () => {
console.error('Failed to read file:', file.name);
};
reader.readAsArrayBuffer(file);
}
// Handle files specifically from drag and drop
handleDragDropFiles(files) {
// Clear the file input to prevent conflicts
const fileInput = document.getElementById('fileInput');
fileInput.value = '';
this.handleFiles(files);
}
// Highlight the process button to indicate action is needed
highlightProcessButton() {
const processButton = document.getElementById('processButton');
if (!processButton.disabled) {
processButton.classList.add('highlight');
// Check if this is a new file or changed settings
const isFirstLoad = this.pointClouds.length === 0;
processButton.textContent = isFirstLoad ?
'🔄 Process File (Ready)' :
'🔄 Process File (Changes Ready)';
}
}
// Remove highlight from process button
unhighlightProcessButton() {
const processButton = document.getElementById('processButton');
processButton.classList.remove('highlight');
processButton.textContent = '🔄 Process File';
}
// Check URL parameters for auto-fetch
checkURLParameters() {
const argument = new URL(document.URL).searchParams.get('fetch');
if (argument) {
console.log('Auto-fetching from URL parameter:', argument);
this.tryFetchAPI(argument);
}
}
toggleControls() {
const controls = document.getElementById('controls');
const restoreButton = document.getElementById('restoreButton');
this.controlsMinimized = !this.controlsMinimized;
if (this.controlsMinimized) {
controls.classList.add('minimized');
restoreButton.style.display = 'flex';
} else {
controls.classList.remove('minimized');
restoreButton.style.display = 'none';
}
}
showExportDialog() {
// Show export dialog
const dialog = document.getElementById('exportDialog');
const filenameInput = document.getElementById('exportFilename');
const formatDiv = document.getElementById('exportFormat');
const cancelButton = document.getElementById('cancelExport');
const confirmButton = document.getElementById('confirmExport');
// Generate default filename from original file
const baseFilename = this.originalFileName.split('.')[0] || 'pointcloud';
filenameInput.value = baseFilename + '_pointcloud';
// Update dialog title and format info
document.querySelector('#exportDialog h3').textContent = 'Export to PLY';
formatDiv.textContent = 'Format: PLY';
// Show dialog
dialog.style.display = 'block';
// Handle cancel
cancelButton.onclick = () => {
dialog.style.display = 'none';
};
// Handle confirm
confirmButton.onclick = () => {
// Get user filename
let filename = filenameInput.value.trim();
// Add default if empty
if (!filename) {
filename = baseFilename + '_pointcloud';
}
// Add extension if not present
const ext = '.ply';
if (!filename.toLowerCase().endsWith(ext)) {
filename += ext;
}
// Hide dialog
dialog.style.display = 'none';
// Perform the actual export
this._generatePLYFile(filename);
};
}
exportToPLY() {
if (this.pointClouds.length === 0) {
console.log('No point clouds to export');
return;
}
this.showExportDialog();
}
// Memory-efficient PLY export using streaming approach
_generatePLYFile(filename) {
// Show loading message
document.getElementById('loadingMessage').style.display = 'block';
document.getElementById('loadingMessage').innerHTML = '<div>📝 Generating PLY file...</div><div style="font-size: 12px; margin-top: 8px; opacity: 0.8;">Preparing export...</div>';
try {
// Extract all Points objects from point clouds (which may be Groups)
const pointObjects = [];
for (const cloudOrGroup of this.pointClouds) {
if (cloudOrGroup.type === 'Points') {
// Direct Points object (old style)
pointObjects.push(cloudOrGroup);
} else if (cloudOrGroup.type === 'Group') {
// Group containing Points and possibly other objects
cloudOrGroup.traverse((child) => {
if (child.type === 'Points') {
pointObjects.push(child);
}
});
}
}
if (pointObjects.length === 0) {
console.error('No point objects found to export');
this._handleExportError('No points to export');
return;
}
// Count total number of vertices
let totalVertices = 0;
for (const pointObj of pointObjects) {
if (pointObj.geometry && pointObj.geometry.attributes && pointObj.geometry.attributes.position) {
totalVertices += pointObj.geometry.attributes.position.count;
} else {
console.warn('Point object missing geometry or position attribute:', pointObj);
}
}
if (totalVertices === 0) {
console.error('No vertices found in point objects');
this._handleExportError('No vertices to export');
return;
}
console.log(`Exporting ${totalVertices.toLocaleString()} vertices from ${pointObjects.length} point objects`);
// Check if dataset is very large and warn user
if (totalVertices > 10000000) { // 10M points
console.log(`Warning: Large dataset detected (${totalVertices.toLocaleString()} points). This may take several minutes.`);
}
// Create PLY header
const header = [
'ply',
'format ascii 1.0',
'comment Created by Binary Point Cloud Viewer',
`element vertex ${totalVertices}`,
'property float x',
'property float y',
'property float z',
'property uchar red',
'property uchar green',
'property uchar blue',
'end_header'
].join('\n') + '\n';
// Use smaller chunks for very large datasets to prevent memory issues
const baseChunkSize = totalVertices > 5000000 ? 25000 : 50000;
let processedVertices = 0;
const chunks = [];
// Add header as first chunk
chunks.push(new Blob([header], { type: 'text/plain' }));
// Process point objects with smaller memory footprint
const processPoints = (objIndex = 0) => {
if (objIndex >= pointObjects.length) {
// All objects processed, create final blob and download
this._downloadBlobChunks(chunks, filename, totalVertices);
return;
}
// Get current point object
const pointObj = pointObjects[objIndex];
const positions = pointObj.geometry.attributes.position.array;
const colors = pointObj.geometry.attributes.color.array;
const count = pointObj.geometry.attributes.position.count;
// Get the point object's world position (from parent group)
let worldPos = new THREE.Vector3();
pointObj.getWorldPosition(worldPos);
// Process points in very small chunks to avoid memory spikes
const processPointsChunk = (startIdx = 0) => {
try {
// Calculate end index for this chunk
const endIdx = Math.min(startIdx + baseChunkSize, count);
// Use array for better performance than string concatenation
const lines = [];
// Add vertices for this chunk
for (let i = startIdx; i < endIdx; i++) {
const idx = i * 3;
// Calculate world coordinates
const x = positions[idx] + worldPos.x;
const y = positions[idx + 1] + worldPos.y;
const z = positions[idx + 2] + worldPos.z;
// Convert normalized colors [0,1] to RGB [0,255]
const r = Math.floor(colors[idx] * 255);
const g = Math.floor(colors[idx + 1] * 255);
const b = Math.floor(colors[idx + 2] * 255);
// Add vertex line
lines.push(`${x} ${y} ${z} ${r} ${g} ${b}`);
}
// Create blob for this chunk and add to chunks array
const chunkContent = lines.join('\n') + '\n';
chunks.push(new Blob([chunkContent], { type: 'text/plain' }));
// Update counters
processedVertices += (endIdx - startIdx);
// Update progress
const overallProgress = (processedVertices / totalVertices * 100).toFixed(1);
const objProgress = (endIdx / count * 100).toFixed(1);
document.getElementById('loadingMessage').innerHTML =
`<div>📝 Generating PLY file...</div><div style="font-size: 11px; margin-top: 8px; opacity: 0.8;">${overallProgress}% (${processedVertices.toLocaleString()}/${totalVertices.toLocaleString()} points)<br>Object ${objIndex+1}/${pointObjects.length}: ${objProgress}%</div>`;
// Process next chunk or next object
if (endIdx < count) {
// Use shorter timeout for smaller chunks to maintain responsiveness
setTimeout(() => processPointsChunk(endIdx), 5);
} else {
setTimeout(() => processPoints(objIndex + 1), 10);
}
} catch (error) {
console.error('Error processing points chunk:', error);
this._handleExportError('Memory error during point processing. Try reducing grid size or chunk size.');
}
};
// Start processing points for this object
processPointsChunk();
};
// Start processing point objects
setTimeout(() => processPoints(), 100);
} catch (error) {
console.error('Error during PLY export setup:', error);
this._handleExportError('Failed to initialize PLY export. The dataset may be too large.');
}
}
// Helper method to download blob chunks efficiently
_downloadBlobChunks(chunks, filename, totalVertices) {
try {
document.getElementById('loadingMessage').innerHTML =
'<div>📝 Finalizing file...</div><div style="font-size: 11px; margin-top: 8px; opacity: 0.8;">Creating download...</div>';
// Create final blob from all chunks
const finalBlob = new Blob(chunks, { type: 'text/plain' });
const url = URL.createObjectURL(finalBlob);
// Download file
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Clean up
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
// Hide loading message
document.getElementById('loadingMessage').style.display = 'none';
console.log(`Successfully exported ${totalVertices.toLocaleString()} points to PLY file: ${filename}`);
} catch (error) {
console.error('Error during file download:', error);
this._handleExportError('Failed to create download file. The file may be too large for your browser.');
}
}
// Helper method to handle export errors gracefully
_handleExportError(message) {
document.getElementById('loadingMessage').innerHTML =
`<div style="color: #ff6b6b;">❌ Export Failed</div><div style="font-size: 11px; margin-top: 8px; opacity: 0.8;">${message}</div>`;
setTimeout(() => {
document.getElementById('loadingMessage').style.display = 'none';
}, 5000);
console.error('PLY Export Error:', message);
}
init() {
try {
// Create scene
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0x000000);
// Create camera
this.camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
this.camera.position.set(0, 0, this.cameraRadius);
this.camera.lookAt(0, 0, 0);
// Create renderer with context loss handling
this.renderer = new THREE.WebGLRenderer({ antialias: false });
this.renderer.setSize(window.innerWidth, window.innerHeight);
document.getElementById('container').appendChild(this.renderer.domElement);
// Add WebGL context loss handlers
const canvas = this.renderer.domElement;
canvas.addEventListener('webglcontextlost', (event) => {
event.preventDefault();
console.error('WebGL context lost! Attempting to recover...');
this.handleContextLost();
}, false);
canvas.addEventListener('webglcontextrestored', () => {
console.log('WebGL context restored! Reinitializing...');
this.handleContextRestored();
}, false);
this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);
// Add grid helper
const gridHelper = new THREE.GridHelper(10, 10);
this.scene.add(gridHelper);
// Add axes helper
const axesHelper = new THREE.AxesHelper(5);
this.scene.add(axesHelper);
// Add ambient light
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
this.scene.add(ambientLight);
// Add directional light
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(1, 1, 1);
this.scene.add(directionalLight);
// Handle window resize
window.addEventListener('resize', () => {
try {
if (this.camera && this.renderer) {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
} catch (error) {
console.error('Error in resize handler:', error);
}
});
console.log('Initialization complete');
} catch (error) {
console.error('Error during initialization:', error);
throw error;
}
}
handleContextLost() {
// Stop the animation loop
this.contextLost = true;
console.log('Context lost - animation loop paused');
}
handleContextRestored() {
// Reinitialize renderer
try {
this.contextLost = false;
// Recreate the scene elements
if (this.pointClouds.length > 0 || this.pathLines.length > 0) {
console.log('Recreating scene after context restoration...');
// The geometries and materials should be recreated automatically by Three.js
}
console.log('Context restored - resuming animation loop');
} catch (error) {
console.error('Error restoring context:', error);
}
}
setupEventListeners() {
const fileInput = document.getElementById('fileInput');
const processButton = document.getElementById('processButton');
const resetButton = document.getElementById('resetButton');
const exportPlyButton = document.getElementById('exportPlyButton');
const minimizeButton = document.getElementById('minimizeButton');
const restoreButton = document.getElementById('restoreButton');
const projectionModeSelect = document.getElementById('projectionMode');
const tupleModeSelect = document.getElementById('tupleMode');
// Control panel minimize/restore functionality
minimizeButton.addEventListener('click', () => {
this.toggleControls();
});
restoreButton.addEventListener('click', () => {
this.toggleControls();
});
// Projection mode change handler
projectionModeSelect.addEventListener('change', () => {
// Show/hide BVH controls based on mode
const isBVHMode = projectionModeSelect.value === 'bvh-with-points' ||
projectionModeSelect.value === 'bvh-only';
document.getElementById('bvhControls').style.display = isBVHMode ? 'flex' : 'none';
document.getElementById('bvhMinPointsControl').style.display = isBVHMode ? 'flex' : 'none';
document.getElementById('bvhLevelControl').style.display = isBVHMode ? 'flex' : 'none';
// Highlight process button to indicate changes need processing
if (this.fileBuffer) {
this.highlightProcessButton();
}
});
// Tuple mode change handler
tupleModeSelect.addEventListener('change', () => {
// Highlight process button to indicate changes need processing
if (this.fileBuffer) {
this.highlightProcessButton();
}
});
// Add change listeners to other processing-related controls
const processingControls = ['dataType', 'startOffset', 'chunkSize', 'gridSize',
'spacing', 'pointSize', 'endianness', 'useQuantization', 'quantizationBits',
'bvhMaxDepth', 'bvhMinPoints', 'bvhDisplayLevel'];
processingControls.forEach(controlId => {
const control = document.getElementById(controlId);
if (control) {
control.addEventListener('change', () => {
if (this.fileBuffer) {
this.highlightProcessButton();
}
});
control.addEventListener('input', () => {
if (this.fileBuffer) {
this.highlightProcessButton();
}
});
}
});
fileInput.addEventListener('change', (event) => {
this.handleFiles(event.target.files);
});
// Also add a click handler to clear the input first (to handle selecting same file twice)
fileInput.addEventListener('click', (event) => {
// Clear the input value so that selecting the same file will trigger change event
event.target.value = '';
});
processButton.addEventListener('click', () => {
if (!this.fileBuffer) return;
this.processFile();
// Enable export button after processing
setTimeout(() => {
exportPlyButton.disabled = false;
}, 100);
});
resetButton.addEventListener('click', () => {
this.resetCamera();
});
exportPlyButton.addEventListener('click', () => {
this.exportToPLY();
});
}
resetCamera() {
try {
if (!this.camera) {
console.error('Cannot reset camera - camera is null');
return;
}
new TWEEN.Tween(this.camera.position)
.to({
x: 0,
y: 0,
z: this.cameraRadius
}, 1000)
.easing(TWEEN.Easing.Quadratic.InOut)
.start();
this.cameraAngle = 0;
} catch (error) {
console.error('Error in resetCamera():', error);
}
}
setupVisibilityHandling() {
// Monitor page visibility to detect when tab becomes inactive
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('Page hidden - idle state started at', new Date().toISOString());
} else {
console.log('Page visible - resuming from idle at', new Date().toISOString());
// Check if renderer is still valid
if (this.renderer) {
const gl = this.renderer.getContext();
if (gl) {
const contextLost = gl.isContextLost();
if (contextLost) {
console.error('WebGL context was lost while page was hidden');
} else {
console.log('WebGL context is still valid');
}
}
}
}
});
}
processFile() {
// Remove highlight when processing starts
this.unhighlightProcessButton();
// Clear existing point clouds and path lines
this.clearPointClouds();
// Get user options
const tupleMode = document.getElementById('tupleMode').value;
const dataType = document.getElementById('dataType').value;
const startOffset = parseInt(document.getElementById('startOffset').value) || 0;
const chunkSizeMB = parseFloat(document.getElementById('chunkSize').value);
const gridSize = parseInt(document.getElementById('gridSize').value);
const spacing = parseFloat(document.getElementById('spacing').value);
const pointSize = parseFloat(document.getElementById('pointSize').value);
const isLittleEndian = document.getElementById('endianness').value === 'true';
const useQuantization = document.getElementById('useQuantization').checked;
const quantizationBits = parseInt(document.getElementById('quantizationBits').value) || 8;
const projectionMode = document.getElementById('projectionMode').value;
// Validate quantization bits
if (quantizationBits < 2 || quantizationBits > 10) {
console.log('Quantization bits must be between 2 and 10, using default 8');
document.getElementById('quantizationBits').value = 8;
return;
}
// Validate start offset
if (startOffset < 0) {
console.log('Start offset cannot be negative, using 0');
document.getElementById('startOffset').value = 0;
return;
}
if (startOffset >= this.fileBuffer.byteLength) {
console.log(`Start offset (${startOffset}) is beyond file size (${this.fileBuffer.byteLength}), using 0`);
document.getElementById('startOffset').value = 0;
return;
}
// Calculate chunk size in bytes
const chunkSize = Math.floor(chunkSizeMB * 1024 * 1024);
// Show loading message
const loadingMsg = document.getElementById('loadingMessage');
loadingMsg.style.display = 'block';
let loadingText = `<div>⏳ Processing data...</div><div style="font-size: 12px; margin-top: 8px; opacity: 0.8;">`;
loadingText += `Mode: <span class="tuple-mode-indicator">${tupleMode.toUpperCase()}</span><br>`;
loadingText += `Projection: ${projectionMode}`;
if (projectionMode === 'continuous-path') {
loadingText += ` <span class="continuous-path-indicator">(PATH)</span>`;
} else if (projectionMode === 'hilbert-curve') {
loadingText += ` <span class="hilbert-indicator">(HILBERT)</span>`;
} else if (projectionMode === 'bvh-with-points') {
loadingText += ` <span class="bvh-indicator">(BVH+PTS)</span>`;
} else if (projectionMode === 'bvh-only') {
loadingText += ` <span class="bvh-indicator">(BVH)</span>`;
} else if (projectionMode === 'lattice-2d') {
loadingText += ` <span class="lattice-indicator">(LATTICE)</span>`;
} else if (projectionMode === 'tiled') {
loadingText += ` <span class="tiled-indicator">(TILED)</span>`;
} else if (projectionMode === 'orthographic-3plane') {
loadingText += ` (3x points)`;
}
loadingText += `<br>`;
if (useQuantization) {
const qRange = Math.pow(2, quantizationBits);
loadingText += `Using ${quantizationBits}-bit quantization (${qRange}³ positions)<br>`;
} else {
loadingText += 'Standard processing<br>';
}
loadingText += `Start offset: ${startOffset} bytes</div>`;
loadingMsg.innerHTML = loadingText;
// Reset total points counter
this.totalPoints = 0;
// Process file asynchronously to allow UI updates
setTimeout(async () => {
await this.createPointCloudLattice(
this.fileBuffer,
dataType,
startOffset,
chunkSize,
gridSize,
spacing,
pointSize,
isLittleEndian,
useQuantization,
quantizationBits,
projectionMode,
tupleMode
);
}, 100);
}
clearPointClouds() {
try {
// Remove point clouds
for (const cloud of this.pointClouds) {
try {
if (cloud && this.scene) {
this.scene.remove(cloud);
// Dispose geometry and material to prevent memory leaks
if (cloud.geometry) cloud.geometry.dispose();
if (cloud.material) cloud.material.dispose();
}
} catch (error) {
console.error('Error removing point cloud:', error);
}
}
this.pointClouds = [];
// Remove path lines
for (const line of this.pathLines) {
try {
if (line && this.scene) {
this.scene.remove(line);
// Dispose geometry and material to prevent memory leaks
if (line.geometry) line.geometry.dispose();
if (line.material) line.material.dispose();
}
} catch (error) {
console.error('Error removing path line:', error);
}
}
this.pathLines = [];
this.totalPoints = 0;
this.updateStatsDisplay();
} catch (error) {
console.error('Error in clearPointClouds():', error);
}
}
updateStatsDisplay() {
const statsDiv = document.getElementById('statsInfo');
if (this.totalPoints > 0) {
const tupleMode = document.getElementById('tupleMode').value;
const useQuantization = document.getElementById('useQuantization').checked;
const quantizationBits = parseInt(document.getElementById('quantizationBits').value) || 8;
const startOffset = parseInt(document.getElementById('startOffset').value) || 0;
const dataType = document.getElementById('dataType').value;
const projectionMode = document.getElementById('projectionMode').value;
const config = DATA_TYPES[dataType];
let statsText = `<strong>Points:</strong> ${this.totalPoints.toLocaleString()}`;
if (this.pathLines.length > 0) {
statsText += `<br><strong>Paths:</strong> ${this.pathLines.length} <span class="continuous-path-indicator">LINES</span>`;
}
if (startOffset > 0) {
statsText += `<br><strong>Offset:</strong> ${startOffset} bytes`;
}
statsText += `<br><strong>Mode:</strong> <span class="tuple-mode-indicator">${tupleMode.toUpperCase()}</span>`;
statsText += `<br><strong>Type:</strong> ${dataType.toUpperCase()}`;
if (config.isFloat) {
statsText += ` (tanh normalized)`;
} else {
statsText += ` (linear normalized)`;
}
statsText += `<br><strong>Projection:</strong> ${projectionMode}`;
if (projectionMode === 'continuous-path') {
statsText += ` <span class="continuous-path-indicator">(PATH)</span>`;
} else if (projectionMode === 'hilbert-curve') {
statsText += ` <span class="hilbert-indicator">(HILBERT)</span>`;
} else if (projectionMode === 'bvh-with-points') {
statsText += ` <span class="bvh-indicator">(BVH+PTS)</span>`;
} else if (projectionMode === 'bvh-only') {
statsText += ` <span class="bvh-indicator">(BVH)</span>`;
} else if (projectionMode === 'lattice-2d') {
statsText += ` <span class="lattice-indicator">(LATTICE)</span>`;
} else if (projectionMode === 'tiled') {
statsText += ` <span class="tiled-indicator">(TILED)</span>`;
}
if (useQuantization) {
const qRange = Math.pow(2, quantizationBits);
statsText += `<br><strong>Method:</strong> ${quantizationBits}-bit quantized (${qRange}³)`;
} else {
statsText += `<br><strong>Method:</strong> Standard`;
}
statsDiv.innerHTML = statsText;
} else {
statsDiv.innerHTML = '';
}
}
async createPointCloudLattice(buffer, dataType, startOffset, chunkSize, gridSize, spacing, pointSize, isLittleEndian, useQuantization, quantizationBits, projectionMode, tupleMode) {
// Apply start offset to buffer
const effectiveBuffer = startOffset > 0 ? buffer.slice(startOffset) : buffer;
// Calculate how many chunks we need
const totalChunks = Math.min(
gridSize * gridSize * gridSize,
Math.floor(effectiveBuffer.byteLength / chunkSize) + 1
);
// Calculate offset from center based on user-defined spacing
const offset = (gridSize - 1) * spacing / 2;
console.log(`Creating point cloud lattice with ${totalChunks} chunks in ${tupleMode} mode, spacing: ${spacing}, quantization: ${useQuantization ? quantizationBits + '-bit' : 'off'}, projection: ${projectionMode}, start offset: ${startOffset}`);
const loadingMsg = document.getElementById('loadingMessage');
const startTime = Date.now();
// Process chunks incrementally with progress updates
const processChunk = async (chunkIndex, x, y, z) => {
// Update progress message
const progress = ((chunkIndex + 1) / totalChunks * 100).toFixed(1);
const elapsedTime = ((Date.now() - startTime) / 1000).toFixed(1);
const pointsProcessed = this.totalPoints.toLocaleString();
const projectionInfo = projectionMode === 'continuous-path' ? ` • PATH` :
projectionMode === 'hilbert-curve' ? ` • HILBERT` :
projectionMode === 'bvh-with-points' ? ` • BVH+PTS` :
projectionMode === 'bvh-only' ? ` • BVH` :
projectionMode === 'lattice-2d' ? ` • LATTICE` :
projectionMode === 'tiled' ? ` • TILED` :
projectionMode === 'orthographic-3plane' ? ` • 3-Plane` : '';
loadingMsg.innerHTML = `
<div>🔄 Processing chunks...</div>
<div style="font-size: 11px; margin-top: 8px; opacity: 0.8;">
Chunk ${chunkIndex + 1}/${totalChunks} (${progress}%)<br>
Points: ${pointsProcessed} • Time: ${elapsedTime}s<br>
Mode: <span class="tuple-mode-indicator">${tupleMode.toUpperCase()}</span> • Position: [${x}, ${y}, ${z}]<br>
Projection: ${projectionMode}${projectionInfo}
</div>
`;
// Calculate chunk position in the grid using user-defined spacing
const posX = (x * spacing) - offset;
const posY = (y * spacing) - offset;
const posZ = (z * spacing) - offset;
// Calculate chunk start and end offsets (relative to effective buffer)
const chunkStartOffset = chunkIndex * chunkSize;
const chunkEndOffset = Math.min(chunkStartOffset + chunkSize, effectiveBuffer.byteLength);
// Check if we have enough data for this chunk
if (chunkStartOffset >= effectiveBuffer.byteLength) {
return false; // Signal to stop processing
}
// Extract chunk buffer
const chunkBuffer = effectiveBuffer.slice(chunkStartOffset, chunkEndOffset);
// Process chunk data using selected method and projection
const processedData = quantizeProcessDataAs(chunkBuffer, dataType, isLittleEndian, quantizationBits, projectionMode, tupleMode);
// Create point cloud and optionally path lines
const pointCloud = this.createPointCloud(
processedData,
pointSize,
posX,
posY,
posZ
);
// Add to scene
this.scene.add(pointCloud);
this.pointClouds.push(pointCloud);
// Add to total points count
this.totalPoints += processedData.numPoints;
return true; // Continue processing
};
// Process all chunks with progress updates
let chunkIndex = 0;
for (let x = 0; x < gridSize && chunkIndex < totalChunks; x++) {
for (let y = 0; y < gridSize && chunkIndex < totalChunks; y++) {
for (let z = 0; z < gridSize && chunkIndex < totalChunks; z++) {
// Process this chunk
const shouldContinue = await processChunk(chunkIndex, x, y, z);
if (!shouldContinue) break;
chunkIndex++;
// Add small delay to allow UI updates (every few chunks)
if (chunkIndex % 3 === 0) {
await new Promise(resolve => setTimeout(resolve, 10));
}
}
}
}
// Final progress update
const totalTime = ((Date.now() - startTime) / 1000).toFixed(1);
loadingMsg.innerHTML = `
<div>✅ Processing complete!</div>
<div style="font-size: 11px; margin-top: 8px; opacity: 0.8;">
${this.pointClouds.length} clouds • ${this.totalPoints.toLocaleString()} points<br>
${this.pathLines.length > 0 ? `${this.pathLines.length} paths • ` : ''}Mode: <span class="tuple-mode-indicator">${tupleMode.toUpperCase()}</span> • Projection: ${projectionMode}<br>
Completed in ${totalTime}s
</div>
`;
// Adjust camera distance based on grid size and spacing
this.cameraRadius = Math.max(5, (gridSize * spacing) * 1.2);
this.resetCamera();
console.log(`Created ${this.pointClouds.length} point clouds with ${this.totalPoints.toLocaleString()} total points and ${this.pathLines.length} path lines in ${totalTime}s using ${tupleMode} mode with ${projectionMode} projection`);
// Hide loading message after a short delay
setTimeout(() => {
loadingMsg.style.display = 'none';
this.updateStatsDisplay();
}, 1500);
}
createPointCloud(processedData, pointSize, x, y, z) {
const { points, colors, numPoints, pathData, bvhNodes, bvhMode, showPoints } = processedData;
// Create container group for this chunk
const group = new THREE.Group();
group.position.set(x, y, z);
// Handle BVH mode
if (bvhMode && bvhNodes && bvhNodes.length > 0) {
// Create instanced BVH boxes
const bvhBoxes = createInstancedBVHBoxes(bvhNodes);
if (bvhBoxes) {
group.add(bvhBoxes);
console.log(`Added ${bvhNodes.length} BVH boxes to scene`);
}
// Optionally add points if in "bvh-with-points" mode
if (showPoints && numPoints > 0) {
const pointCloud = this.createPointGeometry(points, colors, numPoints, pointSize);
group.add(pointCloud);
console.log(`Added ${numPoints} points with BVH boxes`);
}
// Return early - don't process as standard point cloud
return group;
}
// Standard mode - create point cloud geometry
if (numPoints > 0) {
const pointCloud = this.createPointGeometry(points, colors, numPoints, pointSize);
group.add(pointCloud);
// If this is continuous path mode or Hilbert curve, also create line geometry
if (pathData && numPoints > 1) {
const actualPoints = points.slice(0, numPoints * 3);
const actualColors = colors.slice(0, numPoints * 3);
// Create line geometry connecting consecutive points
const lineGeometry = new THREE.BufferGeometry();
// Create line positions - same as point positions
lineGeometry.setAttribute('position', new THREE.BufferAttribute(actualPoints, 3));
// Create line colors - same as point colors
lineGeometry.setAttribute('color', new THREE.BufferAttribute(actualColors, 3));
// Create line material with smaller width and transparency
const lineMaterial = new THREE.LineBasicMaterial({
vertexColors: true,
opacity: 0.6,
transparent: true,
linewidth: 1
});
// Create line object
const pathLine = new THREE.Line(lineGeometry, lineMaterial);
// Add to group
group.add(pathLine);
// Track path line separately for cleanup
this.pathLines.push(pathLine);
console.log(`Created path line with ${numPoints} connected points at position [${x}, ${y}, ${z}]`);
}
}
return group;
}
createPointGeometry(points, colors, numPoints, pointSize) {
// Create buffer geometry for points
const geometry = new THREE.BufferGeometry();
// Slice arrays to actual size
const actualPoints = points.slice(0, numPoints * 3);
const actualColors = colors.slice(0, numPoints * 3);
// Set position attributes
const positionAttribute = new THREE.BufferAttribute(actualPoints, 3);
positionAttribute.setUsage(THREE.StaticDrawUsage);
geometry.setAttribute('position', positionAttribute);
// Set color attributes
const colorAttribute = new THREE.BufferAttribute(actualColors, 3);
colorAttribute.setUsage(THREE.StaticDrawUsage);
geometry.setAttribute('color', colorAttribute);
// Create point cloud material
const material = new THREE.PointsMaterial({
size: pointSize,
vertexColors: true,
sizeAttenuation: true
});
// Create points object
const pointCloud = new THREE.Points(geometry, material);
return pointCloud;
}
animate() {
requestAnimationFrame(() => this.animate());
try {
// Skip rendering if context is lost
if (this.contextLost) {
return;
}
// Update TWEEN with error handling
try {
TWEEN.update();
} catch (tweenError) {
console.error('Error in TWEEN.update():', tweenError);
}
// Update controls with error handling
try {
if (this.controls && this.controls.update) {
this.controls.update();
}
} catch (controlsError) {
console.error('Error in controls.update():', controlsError);
}
// Render scene with error handling
try {
if (this.renderer && this.scene && this.camera) {
this.renderer.render(this.scene, this.camera);
} else {
console.error('Missing required objects for rendering:', {
renderer: !!this.renderer,
scene: !!this.scene,
camera: !!this.camera
});
}
} catch (renderError) {
console.error('Error in renderer.render():', renderError);
// Check if this is a context loss
if (renderError.message && renderError.message.includes('context')) {
console.error('Possible context loss detected in render error');
this.contextLost = true;
}
}
} catch (error) {
console.error('Critical error in animation loop:', error);
console.error('Error stack:', error.stack);
}
}
}
// Global error handler to catch crashes
window.addEventListener('error', (event) => {
console.error('Global error caught:', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error
});
// Log additional context
console.error('Error occurred at:', new Date().toISOString());
console.error('Page visibility:', document.hidden ? 'hidden' : 'visible');
// Check if WebGL context might be related
const canvas = document.querySelector('canvas');
if (canvas) {
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl');
if (gl) {
console.error('WebGL context lost?', gl.isContextLost());
}
}
});
// Catch unhandled promise rejections
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
console.error('Rejection type:', typeof event.reason);
console.error('Rejection details:', JSON.stringify(event.reason, Object.getOwnPropertyNames(event.reason)));
if (event.reason && event.reason.stack) {
console.error('Error stack:', event.reason.stack);
}
console.error('Rejection occurred at:', new Date().toISOString());
// Try to get more info
if (event.reason instanceof Error) {
console.error('Error message:', event.reason.message);
console.error('Error name:', event.reason.name);
}
});
// Initialize application
const app = new BinaryPointCloudViewer();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment