Radial tree + Forces
This commit is contained in:
403
src/renderer.ts
403
src/renderer.ts
@@ -32,6 +32,26 @@ void main() {
|
||||
}
|
||||
`;
|
||||
|
||||
const SELECTED_FRAG = `#version 300 es
|
||||
precision mediump float;
|
||||
out vec4 o;
|
||||
void main() {
|
||||
vec2 c = gl_PointCoord * 2.0 - 1.0;
|
||||
if (dot(c, c) > 1.0) discard;
|
||||
o = vec4(1.0, 0.5, 0.0, 0.9); // orange for selected
|
||||
}
|
||||
`;
|
||||
|
||||
const NEIGHBOR_FRAG = `#version 300 es
|
||||
precision mediump float;
|
||||
out vec4 o;
|
||||
void main() {
|
||||
vec2 c = gl_PointCoord * 2.0 - 1.0;
|
||||
if (dot(c, c) > 1.0) discard;
|
||||
o = vec4(1.0, 0.9, 0.0, 0.8); // yellow for neighbors
|
||||
}
|
||||
`;
|
||||
|
||||
/* ── Types ──────────────────────────────────────────────── */
|
||||
|
||||
export interface RenderStats {
|
||||
@@ -43,10 +63,8 @@ export interface RenderStats {
|
||||
|
||||
/* ── Constants ──────────────────────────────────────────── */
|
||||
|
||||
const COUNT = 10_000_000;
|
||||
const EXTENT = 30_000; // particles span [-15000, 15000]
|
||||
const WORLD_RADIUS = 4.0; // sphere world-space radius
|
||||
const MAX_DRAW = 600_000; // max particles to draw per frame
|
||||
const MAX_DRAW = 2_000_000; // max particles to draw per frame
|
||||
|
||||
/* ── Renderer ───────────────────────────────────────────── */
|
||||
|
||||
@@ -55,10 +73,18 @@ export class Renderer {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private program: WebGLProgram;
|
||||
private lineProgram: WebGLProgram;
|
||||
private selectedProgram: WebGLProgram;
|
||||
private neighborProgram: WebGLProgram;
|
||||
private vao: WebGLVertexArrayObject;
|
||||
|
||||
// Data
|
||||
private leaves: Leaf[] = [];
|
||||
private sorted: Float32Array = new Float32Array(0);
|
||||
private nodeCount = 0;
|
||||
private edgeCount = 0;
|
||||
private neighborMap: Map<number, number[]> = new Map();
|
||||
private leafEdgeStarts: Uint32Array = new Uint32Array(0);
|
||||
private leafEdgeCounts: Uint32Array = new Uint32Array(0);
|
||||
private maxPtSize = 256;
|
||||
|
||||
// Multi-draw extension
|
||||
@@ -71,12 +97,28 @@ export class Renderer {
|
||||
private uCenter: WebGLUniformLocation;
|
||||
private uScale: WebGLUniformLocation;
|
||||
private uPtSize: WebGLUniformLocation;
|
||||
|
||||
|
||||
private uCenterLine: WebGLUniformLocation;
|
||||
private uScaleLine: WebGLUniformLocation;
|
||||
|
||||
|
||||
private uCenterSelected: WebGLUniformLocation;
|
||||
private uScaleSelected: WebGLUniformLocation;
|
||||
private uPtSizeSelected: WebGLUniformLocation;
|
||||
|
||||
private uCenterNeighbor: WebGLUniformLocation;
|
||||
private uScaleNeighbor: WebGLUniformLocation;
|
||||
private uPtSizeNeighbor: WebGLUniformLocation;
|
||||
|
||||
private linesIbo: WebGLBuffer;
|
||||
|
||||
// Selection
|
||||
private selectionIbo: WebGLBuffer;
|
||||
private selectionCount = 0;
|
||||
|
||||
// Neighbors
|
||||
private neighborIbo: WebGLBuffer;
|
||||
private neighborCount = 0;
|
||||
|
||||
// Camera state
|
||||
private cx = 0;
|
||||
private cy = 0;
|
||||
@@ -93,16 +135,26 @@ export class Renderer {
|
||||
// Compile programs
|
||||
this.program = this.compileProgram(VERT, FRAG);
|
||||
this.lineProgram = this.compileProgram(VERT, LINE_FRAG);
|
||||
|
||||
this.selectedProgram = this.compileProgram(VERT, SELECTED_FRAG);
|
||||
this.neighborProgram = this.compileProgram(VERT, NEIGHBOR_FRAG);
|
||||
|
||||
gl.useProgram(this.program);
|
||||
|
||||
this.uCenter = gl.getUniformLocation(this.program, "u_center")!;
|
||||
this.uScale = gl.getUniformLocation(this.program, "u_scale")!;
|
||||
this.uPtSize = gl.getUniformLocation(this.program, "u_ptSize")!;
|
||||
|
||||
|
||||
this.uCenterLine = gl.getUniformLocation(this.lineProgram, "u_center")!;
|
||||
this.uScaleLine = gl.getUniformLocation(this.lineProgram, "u_scale")!;
|
||||
|
||||
this.uCenterSelected = gl.getUniformLocation(this.selectedProgram, "u_center")!;
|
||||
this.uScaleSelected = gl.getUniformLocation(this.selectedProgram, "u_scale")!;
|
||||
this.uPtSizeSelected = gl.getUniformLocation(this.selectedProgram, "u_ptSize")!;
|
||||
|
||||
this.uCenterNeighbor = gl.getUniformLocation(this.neighborProgram, "u_center")!;
|
||||
this.uScaleNeighbor = gl.getUniformLocation(this.neighborProgram, "u_scale")!;
|
||||
this.uPtSizeNeighbor = gl.getUniformLocation(this.neighborProgram, "u_ptSize")!;
|
||||
|
||||
// Query hardware max point size
|
||||
const range = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE) as Float32Array;
|
||||
this.maxPtSize = range[1] || 256;
|
||||
@@ -112,13 +164,15 @@ export class Renderer {
|
||||
gl.bindVertexArray(this.vao);
|
||||
const vbo = gl.createBuffer()!;
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
|
||||
|
||||
|
||||
// We forced a_pos to location 0 in compileProgram
|
||||
gl.enableVertexAttribArray(0);
|
||||
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
||||
gl.bindVertexArray(null);
|
||||
|
||||
|
||||
this.linesIbo = gl.createBuffer()!;
|
||||
this.selectionIbo = gl.createBuffer()!;
|
||||
this.neighborIbo = gl.createBuffer()!;
|
||||
|
||||
// Blending
|
||||
gl.enable(gl.BLEND);
|
||||
@@ -127,24 +181,27 @@ export class Renderer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate 10M random particles, build quadtree, upload to GPU.
|
||||
* Load particles from pre-computed positions and edges, build quadtree, upload to GPU.
|
||||
* vertexIds: original vertex IDs from CSV (parallel to xs/ys)
|
||||
* edges: flat array of [srcVertexId, dstVertexId, ...]
|
||||
* Call once at startup. Returns build time in ms.
|
||||
*/
|
||||
init(): number {
|
||||
init(
|
||||
xs: Float32Array,
|
||||
ys: Float32Array,
|
||||
vertexIds: Uint32Array,
|
||||
edges: Uint32Array
|
||||
): number {
|
||||
const t0 = performance.now();
|
||||
const gl = this.gl;
|
||||
|
||||
// Generate random positions as typed arrays (no object allocation)
|
||||
const xs = new Float32Array(COUNT);
|
||||
const ys = new Float32Array(COUNT);
|
||||
for (let i = 0; i < COUNT; i++) {
|
||||
xs[i] = (Math.random() - 0.5) * EXTENT;
|
||||
ys[i] = (Math.random() - 0.5) * EXTENT;
|
||||
}
|
||||
const count = xs.length;
|
||||
const edgeCount = edges.length / 2;
|
||||
this.nodeCount = count;
|
||||
|
||||
// Build quadtree (spatially sorts the array)
|
||||
const { sorted, leaves } = buildSpatialIndex(xs, ys);
|
||||
const { sorted, leaves, order } = buildSpatialIndex(xs, ys);
|
||||
this.leaves = leaves;
|
||||
this.sorted = sorted;
|
||||
|
||||
// Pre-allocate arrays for render loop (zero-allocation rendering)
|
||||
this.visibleLeafIndices = new Uint32Array(leaves.length);
|
||||
@@ -156,56 +213,85 @@ export class Renderer {
|
||||
gl.bufferData(gl.ARRAY_BUFFER, sorted, gl.STATIC_DRAW);
|
||||
gl.bindVertexArray(null);
|
||||
|
||||
// Build relationships (lines)
|
||||
const lineIndices = new Uint32Array(COUNT * 4);
|
||||
for (let j = 0; j < leaves.length; j++) {
|
||||
const lf = leaves[j];
|
||||
const leafSize = lf.end - lf.start;
|
||||
if (leafSize <= 1) continue;
|
||||
// Build vertex ID → original input index mapping
|
||||
const vertexIdToOriginal = new Map<number, number>();
|
||||
for (let i = 0; i < count; i++) {
|
||||
vertexIdToOriginal.set(vertexIds[i], i);
|
||||
}
|
||||
|
||||
for (let i = lf.start; i < lf.end; i++) {
|
||||
const x = sorted[i * 2];
|
||||
const y = sorted[i * 2 + 1];
|
||||
|
||||
let best1 = -1, best2 = -1;
|
||||
let dist1 = Infinity, dist2 = Infinity;
|
||||
|
||||
// Find closest 2 points within a small local sliding window (spatially coherent)
|
||||
const windowSize = Math.min(12, leafSize - 1);
|
||||
for (let w = 1; w <= windowSize; w++) {
|
||||
const candidate = lf.start + ((i - lf.start + w) % leafSize);
|
||||
const cx = sorted[candidate * 2];
|
||||
const cy = sorted[candidate * 2 + 1];
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
const d2 = dx*dx + dy*dy;
|
||||
|
||||
if (d2 < dist1) {
|
||||
dist2 = dist1;
|
||||
best2 = best1;
|
||||
dist1 = d2;
|
||||
best1 = candidate;
|
||||
} else if (d2 < dist2) {
|
||||
dist2 = d2;
|
||||
best2 = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback
|
||||
if (best1 === -1) best1 = lf.start + ((i - lf.start + 1) % leafSize);
|
||||
if (best2 === -1) best2 = lf.start + ((i - lf.start + 2) % leafSize);
|
||||
// Build original input index → sorted index mapping
|
||||
// order[sortedIdx] = originalIdx, so invert it
|
||||
const originalToSorted = new Uint32Array(count);
|
||||
for (let i = 0; i < count; i++) {
|
||||
originalToSorted[order[i]] = i;
|
||||
}
|
||||
|
||||
const base = i * 4;
|
||||
lineIndices[base + 0] = i;
|
||||
lineIndices[base + 1] = best1;
|
||||
lineIndices[base + 2] = i;
|
||||
lineIndices[base + 3] = best2;
|
||||
// Remap edges from vertex IDs to sorted indices
|
||||
const lineIndices = new Uint32Array(edgeCount * 2);
|
||||
let validEdges = 0;
|
||||
for (let i = 0; i < edgeCount; i++) {
|
||||
const srcId = edges[i * 2];
|
||||
const dstId = edges[i * 2 + 1];
|
||||
const srcOrig = vertexIdToOriginal.get(srcId);
|
||||
const dstOrig = vertexIdToOriginal.get(dstId);
|
||||
if (srcOrig === undefined || dstOrig === undefined) continue;
|
||||
lineIndices[validEdges * 2] = originalToSorted[srcOrig];
|
||||
lineIndices[validEdges * 2 + 1] = originalToSorted[dstOrig];
|
||||
validEdges++;
|
||||
}
|
||||
this.edgeCount = validEdges;
|
||||
|
||||
// Build per-node neighbor list from edges for selection queries
|
||||
const neighborMap = new Map<number, number[]>();
|
||||
for (let i = 0; i < validEdges; i++) {
|
||||
const src = lineIndices[i * 2];
|
||||
const dst = lineIndices[i * 2 + 1];
|
||||
if (!neighborMap.has(src)) neighborMap.set(src, []);
|
||||
neighborMap.get(src)!.push(dst);
|
||||
if (!neighborMap.has(dst)) neighborMap.set(dst, []);
|
||||
neighborMap.get(dst)!.push(src);
|
||||
}
|
||||
this.neighborMap = neighborMap;
|
||||
|
||||
// Build per-leaf edge index for efficient visible-only edge drawing
|
||||
// Find which leaf each sorted index belongs to
|
||||
const nodeToLeaf = new Uint32Array(count);
|
||||
for (let li = 0; li < leaves.length; li++) {
|
||||
const lf = leaves[li];
|
||||
for (let j = lf.start; j < lf.end; j++) {
|
||||
nodeToLeaf[j] = li;
|
||||
}
|
||||
}
|
||||
|
||||
// Upload lines to GPU (NOT bound to VAO)
|
||||
// Count edges per leaf (by source node)
|
||||
const leafEdgeCounts = new Uint32Array(leaves.length);
|
||||
for (let i = 0; i < validEdges; i++) {
|
||||
leafEdgeCounts[nodeToLeaf[lineIndices[i * 2]]]++;
|
||||
}
|
||||
|
||||
// Compute prefix sums for edge offsets per leaf
|
||||
const leafEdgeOffsets = new Uint32Array(leaves.length);
|
||||
for (let i = 1; i < leaves.length; i++) {
|
||||
leafEdgeOffsets[i] = leafEdgeOffsets[i - 1] + leafEdgeCounts[i - 1];
|
||||
}
|
||||
|
||||
// Sort edges by source leaf into a new buffer
|
||||
const sortedEdgeIndices = new Uint32Array(validEdges * 2);
|
||||
const leafEdgeCurrent = new Uint32Array(leaves.length);
|
||||
for (let i = 0; i < validEdges; i++) {
|
||||
const leafIdx = nodeToLeaf[lineIndices[i * 2]];
|
||||
const pos = leafEdgeOffsets[leafIdx] + leafEdgeCurrent[leafIdx];
|
||||
sortedEdgeIndices[pos * 2] = lineIndices[i * 2];
|
||||
sortedEdgeIndices[pos * 2 + 1] = lineIndices[i * 2 + 1];
|
||||
leafEdgeCurrent[leafIdx]++;
|
||||
}
|
||||
|
||||
this.leafEdgeStarts = leafEdgeOffsets;
|
||||
this.leafEdgeCounts = leafEdgeCounts;
|
||||
|
||||
// Upload sorted edges to GPU
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.linesIbo);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, lineIndices, gl.STATIC_DRAW);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, sortedEdgeIndices, gl.STATIC_DRAW);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
|
||||
|
||||
return performance.now() - t0;
|
||||
@@ -241,6 +327,135 @@ export class Renderer {
|
||||
return this.zoom;
|
||||
}
|
||||
|
||||
getNodeCount(): number {
|
||||
return this.nodeCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert screen coordinates (CSS pixels) to world coordinates.
|
||||
*/
|
||||
screenToWorld(screenX: number, screenY: number): { x: number; y: number } {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const px = screenX * dpr;
|
||||
const py = screenY * dpr;
|
||||
const wx = this.cx + (px - this.canvas.width / 2) / this.zoom;
|
||||
const wy = this.cy + (py - this.canvas.height / 2) / this.zoom;
|
||||
return { x: wx, y: wy };
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the node closest to the given screen position.
|
||||
* Uses the quadtree to narrow down the search.
|
||||
* Returns the node's world coordinates if found within the visual radius, or null.
|
||||
*/
|
||||
findNodeAt(screenX: number, screenY: number): { x: number; y: number } | null {
|
||||
const result = this.findNodeIndexAt(screenX, screenY);
|
||||
return result ? { x: result.x, y: result.y } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the node closest to the given screen position.
|
||||
* Returns the node's index and world coordinates if found, or null.
|
||||
*/
|
||||
findNodeIndexAt(screenX: number, screenY: number): { index: number; x: number; y: number } | null {
|
||||
if (this.sorted.length === 0) return null;
|
||||
|
||||
const world = this.screenToWorld(screenX, screenY);
|
||||
const wx = world.x;
|
||||
const wy = world.y;
|
||||
|
||||
// Calculate the search radius in world units (based on point size on screen)
|
||||
// We use a slightly larger radius for easier hovering
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const ptSizeScreen = Math.max(1.0, Math.min(this.maxPtSize, WORLD_RADIUS * 2 * this.zoom));
|
||||
const hitRadius = (ptSizeScreen / this.zoom / dpr) * 0.75; // world units
|
||||
const hitRadiusSq = hitRadius * hitRadius;
|
||||
|
||||
let closestDist = Infinity;
|
||||
let closestIndex = -1;
|
||||
let closestX = 0;
|
||||
let closestY = 0;
|
||||
|
||||
// Traverse all leaves and check if they intersect with the hit area
|
||||
for (let i = 0; i < this.leaves.length; i++) {
|
||||
const lf = this.leaves[i];
|
||||
|
||||
// Quick AABB check: does this leaf possibly contain points near our target?
|
||||
if (
|
||||
wx + hitRadius < lf.minX ||
|
||||
wx - hitRadius > lf.maxX ||
|
||||
wy + hitRadius < lf.minY ||
|
||||
wy - hitRadius > lf.maxY
|
||||
) {
|
||||
continue; // Leaf is too far away
|
||||
}
|
||||
|
||||
// Check all points in this leaf
|
||||
for (let j = lf.start; j < lf.end; j++) {
|
||||
const px = this.sorted[j * 2];
|
||||
const py = this.sorted[j * 2 + 1];
|
||||
const dx = px - wx;
|
||||
const dy = py - wy;
|
||||
const distSq = dx * dx + dy * dy;
|
||||
|
||||
if (distSq < hitRadiusSq && distSq < closestDist) {
|
||||
closestDist = distSq;
|
||||
closestIndex = j;
|
||||
closestX = px;
|
||||
closestY = py;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return closestIndex >= 0 ? { index: closestIndex, x: closestX, y: closestY } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the selection buffer with the given set of node indices.
|
||||
* Also computes neighbors of selected nodes.
|
||||
* Call this whenever React's selection state changes.
|
||||
*/
|
||||
updateSelection(selectedIndices: Set<number>): void {
|
||||
const gl = this.gl;
|
||||
|
||||
// Upload selected indices
|
||||
const indices = new Uint32Array(selectedIndices);
|
||||
this.selectionCount = indices.length;
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.selectionIbo);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.DYNAMIC_DRAW);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
|
||||
|
||||
// Compute neighbors of selected nodes (excluding already selected)
|
||||
const neighborSet = new Set<number>();
|
||||
for (const nodeIdx of selectedIndices) {
|
||||
const nodeNeighbors = this.neighborMap.get(nodeIdx);
|
||||
if (!nodeNeighbors) continue;
|
||||
for (const n of nodeNeighbors) {
|
||||
if (!selectedIndices.has(n)) {
|
||||
neighborSet.add(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upload neighbor indices
|
||||
const neighborIndices = new Uint32Array(neighborSet);
|
||||
this.neighborCount = neighborIndices.length;
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.neighborIbo);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, neighborIndices, gl.DYNAMIC_DRAW);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the coordinates of a node by its index.
|
||||
*/
|
||||
getNodeCoords(index: number): { x: number; y: number } | null {
|
||||
if (index < 0 || index * 2 + 1 >= this.sorted.length) return null;
|
||||
return {
|
||||
x: this.sorted[index * 2],
|
||||
y: this.sorted[index * 2 + 1],
|
||||
};
|
||||
}
|
||||
|
||||
/* ── Render ───────────────────────────────────────────── */
|
||||
|
||||
render(): RenderStats {
|
||||
@@ -297,7 +512,7 @@ export class Renderer {
|
||||
lf.minY > vMaxY
|
||||
)
|
||||
continue;
|
||||
|
||||
|
||||
this.visibleLeafIndices[visibleCount++] = i;
|
||||
totalVisibleParticles += (lf.end - lf.start);
|
||||
}
|
||||
@@ -306,17 +521,17 @@ export class Renderer {
|
||||
const ratio = Math.min(1.0, MAX_DRAW / Math.max(1, totalVisibleParticles));
|
||||
|
||||
let drawnCount = 0;
|
||||
|
||||
|
||||
// 3. Prepare index/count arrays for drawing
|
||||
for (let i = 0; i < visibleCount; i++) {
|
||||
const leafIdx = this.visibleLeafIndices[i];
|
||||
const lf = this.leaves[leafIdx];
|
||||
const leafTotal = lf.end - lf.start;
|
||||
|
||||
|
||||
// Since the leaf is randomly unordered internally, taking the first N points
|
||||
// is a perfect uniform spatial sample of this leaf.
|
||||
const drawCount = Math.max(1, Math.floor(leafTotal * ratio));
|
||||
|
||||
|
||||
this.startsArray[i] = lf.start;
|
||||
this.countsArray[i] = drawCount;
|
||||
drawnCount += drawCount;
|
||||
@@ -353,28 +568,52 @@ export class Renderer {
|
||||
gl.useProgram(this.lineProgram);
|
||||
gl.uniform2f(this.uCenterLine, this.cx, this.cy);
|
||||
gl.uniform2f(this.uScaleLine, (this.zoom * 2) / cw, (-this.zoom * 2) / ch);
|
||||
|
||||
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.linesIbo);
|
||||
|
||||
|
||||
for (let i = 0; i < visibleCount; i++) {
|
||||
const leafIdx = this.visibleLeafIndices[i];
|
||||
const lf = this.leaves[leafIdx];
|
||||
const leafTotal = lf.end - lf.start;
|
||||
if (leafTotal <= 1) continue;
|
||||
// Each node has 4 indices (2 lines)
|
||||
// Offset is in bytes: start index * 4 (indices per point) * 4 (bytes per uint32)
|
||||
gl.drawElements(gl.LINES, leafTotal * 4, gl.UNSIGNED_INT, lf.start * 16);
|
||||
const edgeCount = this.leafEdgeCounts[leafIdx];
|
||||
if (edgeCount === 0) continue;
|
||||
// Each edge is 2 indices (1 line segment)
|
||||
// Offset is in bytes: edgeStart * 2 (indices per edge) * 4 (bytes per uint32)
|
||||
const edgeStart = this.leafEdgeStarts[leafIdx];
|
||||
gl.drawElements(gl.LINES, edgeCount * 2, gl.UNSIGNED_INT, edgeStart * 2 * 4);
|
||||
}
|
||||
|
||||
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
|
||||
}
|
||||
|
||||
// 6. Draw Neighbor Nodes (yellow) - drawn before selected so selected appears on top
|
||||
if (this.neighborCount > 0) {
|
||||
gl.useProgram(this.neighborProgram);
|
||||
gl.uniform2f(this.uCenterNeighbor, this.cx, this.cy);
|
||||
gl.uniform2f(this.uScaleNeighbor, (this.zoom * 2) / cw, (-this.zoom * 2) / ch);
|
||||
gl.uniform1f(this.uPtSizeNeighbor, ptSize);
|
||||
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.neighborIbo);
|
||||
gl.drawElements(gl.POINTS, this.neighborCount, gl.UNSIGNED_INT, 0);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
|
||||
}
|
||||
|
||||
// 7. Draw Selected Nodes on top (orange)
|
||||
if (this.selectionCount > 0) {
|
||||
gl.useProgram(this.selectedProgram);
|
||||
gl.uniform2f(this.uCenterSelected, this.cx, this.cy);
|
||||
gl.uniform2f(this.uScaleSelected, (this.zoom * 2) / cw, (-this.zoom * 2) / ch);
|
||||
gl.uniform1f(this.uPtSizeSelected, ptSize);
|
||||
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.selectionIbo);
|
||||
gl.drawElements(gl.POINTS, this.selectionCount, gl.UNSIGNED_INT, 0);
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, null);
|
||||
}
|
||||
|
||||
gl.bindVertexArray(null);
|
||||
|
||||
const mode = ratio === 1.0
|
||||
? '100% visible nodes'
|
||||
const mode = ratio === 1.0
|
||||
? '100% visible nodes'
|
||||
: ((ratio * 100).toFixed(1) + '% of visible nodes');
|
||||
|
||||
|
||||
return { drawnCount, mode, zoom: this.zoom, ptSize };
|
||||
}
|
||||
|
||||
@@ -397,10 +636,10 @@ export class Renderer {
|
||||
const prog = gl.createProgram()!;
|
||||
gl.attachShader(prog, vs);
|
||||
gl.attachShader(prog, fs);
|
||||
|
||||
|
||||
// Force a_pos to location 0 before linking so both programs match VAO
|
||||
gl.bindAttribLocation(prog, 0, "a_pos");
|
||||
|
||||
|
||||
gl.linkProgram(prog);
|
||||
if (!gl.getProgramParameter(prog, gl.LINK_STATUS))
|
||||
throw new Error("Link: " + gl.getProgramInfoLog(prog));
|
||||
|
||||
Reference in New Issue
Block a user