diff --git a/examples/custom-rendering/node.border.frag.glsl b/examples/custom-rendering/node.border.frag.glsl index 3e8e1dfec..a562e8e46 100644 --- a/examples/custom-rendering/node.border.frag.glsl +++ b/examples/custom-rendering/node.border.frag.glsl @@ -11,6 +11,13 @@ void main(void) { vec4 white = vec4(1.0, 1.0, 1.0, 1.0); float distToCenter = length(gl_PointCoord - vec2(0.5, 0.5)); + #ifdef PICKING_MODE + if (distToCenter < radius) + gl_FragColor = v_color; + else + gl_FragColor = transparent; + #else + // For normal mode, we use the color: float t = 0.0; if (distToCenter < halfRadius - v_border) gl_FragColor = white; @@ -22,4 +29,5 @@ void main(void) { gl_FragColor = mix(transparent, v_color, (radius - distToCenter) / v_border); else gl_FragColor = transparent; + #endif } diff --git a/examples/custom-rendering/node.border.ts b/examples/custom-rendering/node.border.ts index 97d74a5d6..0b2fca16f 100644 --- a/examples/custom-rendering/node.border.ts +++ b/examples/custom-rendering/node.border.ts @@ -17,44 +17,44 @@ import { floatColor } from "sigma/utils"; import { NodeProgram } from "sigma/rendering/webgl/programs/common/node"; import VERTEX_SHADER_SOURCE from "!raw-loader!./node.border.vert.glsl"; import FRAGMENT_SHADER_SOURCE from "!raw-loader!./node.border.frag.glsl"; +import { ProgramInfo } from "sigma/rendering/webgl/programs/common/program"; const { UNSIGNED_BYTE, FLOAT } = WebGLRenderingContext; const UNIFORMS = ["u_sizeRatio", "u_pixelRatio", "u_matrix"] as const; -export default class NodeBorderProgram extends NodeProgram { +export default class NodeBorderProgram extends NodeProgram<(typeof UNIFORMS)[number]> { getDefinition() { return { VERTICES: 1, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE, + METHOD: WebGLRenderingContext.POINTS, UNIFORMS, ATTRIBUTES: [ { name: "a_position", size: 2, type: FLOAT }, { name: "a_size", size: 1, type: FLOAT }, { name: "a_color", size: 4, type: UNSIGNED_BYTE, normalized: true }, + { name: "a_id", size: 4, type: UNSIGNED_BYTE, normalized: true }, ], }; } - processVisibleItem(i: number, data: NodeDisplayData) { + processVisibleItem(nodeIndex: number, startIndex: number, data: NodeDisplayData) { const array = this.array; - array[i++] = data.x; - array[i++] = data.y; - array[i++] = data.size; - array[i] = floatColor(data.color); + array[startIndex++] = data.x; + array[startIndex++] = data.y; + array[startIndex++] = data.size; + array[startIndex++] = floatColor(data.color); + array[startIndex++] = nodeIndex; } - draw(params: RenderParams): void { - const gl = this.gl; - - const { u_sizeRatio, u_pixelRatio, u_matrix } = this.uniformLocations; + setUniforms(params: RenderParams, { gl, uniformLocations }: ProgramInfo): void { + const { u_sizeRatio, u_pixelRatio, u_matrix } = uniformLocations; gl.uniform1f(u_sizeRatio, params.sizeRatio); gl.uniform1f(u_pixelRatio, params.pixelRatio); gl.uniformMatrix3fv(u_matrix, false, params.matrix); - - gl.drawArrays(gl.POINTS, 0, this.verticesCount); } } diff --git a/examples/custom-rendering/node.border.vert.glsl b/examples/custom-rendering/node.border.vert.glsl index 1254b1f80..fe1f3f6b8 100644 --- a/examples/custom-rendering/node.border.vert.glsl +++ b/examples/custom-rendering/node.border.vert.glsl @@ -1,6 +1,7 @@ +attribute vec4 a_id; +attribute vec4 a_color; attribute vec2 a_position; attribute float a_size; -attribute vec4 a_color; uniform float u_sizeRatio; uniform float u_pixelRatio; @@ -25,7 +26,12 @@ void main() { v_border = (0.5 / a_size) * u_sizeRatio; - // Extract the color: + #ifdef PICKING_MODE + // For picking mode, we use the ID as the color: + v_color = a_id; + #else + // For normal mode, we use the color: v_color = a_color; v_color.a *= bias; + #endif } diff --git a/src/core/labels.ts b/src/core/labels.ts index c117bb66f..7cb267485 100644 --- a/src/core/labels.ts +++ b/src/core/labels.ts @@ -2,7 +2,7 @@ * Sigma.js Labels Heuristics * =========================== * - * Miscelleneous heuristics related to label display. + * Miscellaneous heuristics related to label display. * @module */ import Graph from "graphology-types"; diff --git a/src/core/quadtree.ts b/src/core/quadtree.ts deleted file mode 100644 index 4afe5651c..000000000 --- a/src/core/quadtree.ts +++ /dev/null @@ -1,549 +0,0 @@ -/** - * Sigma.js Quad Tree Class - * ========================= - * - * Class implementing the quad tree data structure used to solve hovers and - * determine which elements are currently in the scope of the camera so that - * we don't waste time rendering things the user cannot see anyway. - * @module - */ -/* eslint no-nested-ternary: 0 */ -/* eslint no-constant-condition: 0 */ -import extend from "@yomguithereal/helpers/extend"; - -/** - * Notes: - * - * - a square can be represented as topleft + width, saying for the quad blocks, - * to reduce overall memory usage (which is already pretty low). - * - this implementation of a quadtree is often called a MX-CIF quadtree. - * - we could explore spatial hashing (hilbert quadtrees, notably). - */ - -/** - * Constants. - * - * Note that since we are representing a static 4-ary tree, the indices of the - * quadrants are the following: - * - TOP_LEFT: 4i + b - * - TOP_RIGHT: 4i + 2b - * - BOTTOM_LEFT: 4i + 3b - * - BOTTOM_RIGHT: 4i + 4b - */ -const BLOCKS = 4; -const MAX_LEVEL = 5; - -// Outside block is max block index + 1, i.e.: -// BLOCKS * ((4 * (4 ** MAX_LEVEL) - 1) / 3) -const OUTSIDE_BLOCK = 5460; - -const X_OFFSET = 0; -const Y_OFFSET = 1; -const WIDTH_OFFSET = 2; -const HEIGHT_OFFSET = 3; - -const TOP_LEFT = 1; -const TOP_RIGHT = 2; -const BOTTOM_LEFT = 3; -const BOTTOM_RIGHT = 4; - -let hasWarnedTooMuchOutside = false; - -export interface Boundaries { - x: number; - y: number; - width: number; - height: number; -} - -export interface Rectangle { - x1: number; - y1: number; - x2: number; - y2: number; - height: number; -} - -export interface Vector { - x: number; - y: number; -} - -/** - * Geometry helpers. - */ - -/** - * Function returning whether the given rectangle is axis-aligned. - * - * @param {Rectangle} rect - * @return {boolean} - */ -export function isRectangleAligned(rect: Rectangle): boolean { - return rect.x1 === rect.x2 || rect.y1 === rect.y2; -} - -/** - * Function returning the smallest rectangle that contains the given rectangle, and that is aligned with the axis. - * - * @param {Rectangle} rect - * @return {Rectangle} - */ -export function getCircumscribedAlignedRectangle(rect: Rectangle): Rectangle { - const width = Math.sqrt(Math.pow(rect.x2 - rect.x1, 2) + Math.pow(rect.y2 - rect.y1, 2)); - const heightVector = { - x: ((rect.y1 - rect.y2) * rect.height) / width, - y: ((rect.x2 - rect.x1) * rect.height) / width, - }; - - // Compute all corners: - const tl: Vector = { x: rect.x1, y: rect.y1 }; - const tr: Vector = { x: rect.x2, y: rect.y2 }; - const bl: Vector = { - x: rect.x1 + heightVector.x, - y: rect.y1 + heightVector.y, - }; - const br: Vector = { - x: rect.x2 + heightVector.x, - y: rect.y2 + heightVector.y, - }; - - const xL = Math.min(tl.x, tr.x, bl.x, br.x); - const xR = Math.max(tl.x, tr.x, bl.x, br.x); - const yT = Math.min(tl.y, tr.y, bl.y, br.y); - const yB = Math.max(tl.y, tr.y, bl.y, br.y); - - return { - x1: xL, - y1: yT, - x2: xR, - y2: yT, - height: yB - yT, - }; -} - -/** - * - * @param x1 - * @param y1 - * @param w - * @param qx - * @param qy - * @param qw - * @param qh - */ -export function squareCollidesWithQuad( - x1: number, - y1: number, - w: number, - qx: number, - qy: number, - qw: number, - qh: number, -): boolean { - return x1 < qx + qw && x1 + w > qx && y1 < qy + qh && y1 + w > qy; -} - -export function rectangleCollidesWithQuad( - x1: number, - y1: number, - w: number, - h: number, - qx: number, - qy: number, - qw: number, - qh: number, -): boolean { - return x1 < qx + qw && x1 + w > qx && y1 < qy + qh && y1 + h > qy; -} - -function pointIsInQuad(x: number, y: number, qx: number, qy: number, qw: number, qh: number): number { - const xmp = qx + qw / 2, - ymp = qy + qh / 2, - top = y < ymp, - left = x < xmp; - - return top ? (left ? TOP_LEFT : TOP_RIGHT) : left ? BOTTOM_LEFT : BOTTOM_RIGHT; -} - -/** - * Helper functions that are not bound to the class so an external user - * cannot mess with them. - */ -function buildQuadrants(maxLevel: number, data: Float32Array): void { - // [block, level] - const stack: Array = [0, 0]; - - while (stack.length) { - const level = stack.pop() as number, - block = stack.pop() as number; - - const topLeftBlock = 4 * block + BLOCKS, - topRightBlock = 4 * block + 2 * BLOCKS, - bottomLeftBlock = 4 * block + 3 * BLOCKS, - bottomRightBlock = 4 * block + 4 * BLOCKS; - - const x = data[block + X_OFFSET], - y = data[block + Y_OFFSET], - width = data[block + WIDTH_OFFSET], - height = data[block + HEIGHT_OFFSET], - hw = width / 2, - hh = height / 2; - - data[topLeftBlock + X_OFFSET] = x; - data[topLeftBlock + Y_OFFSET] = y; - data[topLeftBlock + WIDTH_OFFSET] = hw; - data[topLeftBlock + HEIGHT_OFFSET] = hh; - - data[topRightBlock + X_OFFSET] = x + hw; - data[topRightBlock + Y_OFFSET] = y; - data[topRightBlock + WIDTH_OFFSET] = hw; - data[topRightBlock + HEIGHT_OFFSET] = hh; - - data[bottomLeftBlock + X_OFFSET] = x; - data[bottomLeftBlock + Y_OFFSET] = y + hh; - data[bottomLeftBlock + WIDTH_OFFSET] = hw; - data[bottomLeftBlock + HEIGHT_OFFSET] = hh; - - data[bottomRightBlock + X_OFFSET] = x + hw; - data[bottomRightBlock + Y_OFFSET] = y + hh; - data[bottomRightBlock + WIDTH_OFFSET] = hw; - data[bottomRightBlock + HEIGHT_OFFSET] = hh; - - if (level < maxLevel - 1) { - stack.push(bottomRightBlock, level + 1); - stack.push(bottomLeftBlock, level + 1); - stack.push(topRightBlock, level + 1); - stack.push(topLeftBlock, level + 1); - } - } -} - -function insertNode( - maxLevel: number, - data: Float32Array, - containers: Record, - key: string, - x: number, - y: number, - size: number, -) { - const x1 = x - size, - y1 = y - size, - w = size * 2; - - let level = 0, - block = 0; - - while (true) { - // If we reached max level - if (level >= maxLevel) { - containers[block] = containers[block] || []; - containers[block].push(key); - return; - } - - const topLeftBlock = 4 * block + BLOCKS, - topRightBlock = 4 * block + 2 * BLOCKS, - bottomLeftBlock = 4 * block + 3 * BLOCKS, - bottomRightBlock = 4 * block + 4 * BLOCKS; - - const collidingWithTopLeft = squareCollidesWithQuad( - x1, - y1, - w, - data[topLeftBlock + X_OFFSET], - data[topLeftBlock + Y_OFFSET], - data[topLeftBlock + WIDTH_OFFSET], - data[topLeftBlock + HEIGHT_OFFSET], - ); - - const collidingWithTopRight = squareCollidesWithQuad( - x1, - y1, - w, - data[topRightBlock + X_OFFSET], - data[topRightBlock + Y_OFFSET], - data[topRightBlock + WIDTH_OFFSET], - data[topRightBlock + HEIGHT_OFFSET], - ); - - const collidingWithBottomLeft = squareCollidesWithQuad( - x1, - y1, - w, - data[bottomLeftBlock + X_OFFSET], - data[bottomLeftBlock + Y_OFFSET], - data[bottomLeftBlock + WIDTH_OFFSET], - data[bottomLeftBlock + HEIGHT_OFFSET], - ); - - const collidingWithBottomRight = squareCollidesWithQuad( - x1, - y1, - w, - data[bottomRightBlock + X_OFFSET], - data[bottomRightBlock + Y_OFFSET], - data[bottomRightBlock + WIDTH_OFFSET], - data[bottomRightBlock + HEIGHT_OFFSET], - ); - - const collisions: number = [ - collidingWithTopLeft, - collidingWithTopRight, - collidingWithBottomLeft, - collidingWithBottomRight, - ].reduce((acc: number, current: boolean) => { - if (current) return acc + 1; - else return acc; - }, 0); - - // If we have no collision at root level, inject node in the outside block - if (collisions === 0 && level === 0) { - containers[OUTSIDE_BLOCK].push(key); - - if (!hasWarnedTooMuchOutside && containers[OUTSIDE_BLOCK].length >= 5) { - hasWarnedTooMuchOutside = true; - console.warn( - "sigma/quadtree.insertNode: At least 5 nodes are outside the global quadtree zone. " + - "You might have a problem with the normalization function or the custom bounding box.", - ); - } - - return; - } - - // If we don't have at least a collision but deeper, there is an issue - if (collisions === 0) - throw new Error( - `sigma/quadtree.insertNode: no collision (level: ${level}, key: ${key}, x: ${x}, y: ${y}, size: ${size}).`, - ); - - // If we have 3 collisions, we have a geometry problem obviously - if (collisions === 3) - throw new Error( - `sigma/quadtree.insertNode: 3 impossible collisions (level: ${level}, key: ${key}, x: ${x}, y: ${y}, size: ${size}).`, - ); - - // If we have more that one collision, we stop here and store the node - // in the relevant containers - if (collisions > 1) { - containers[block] = containers[block] || []; - containers[block].push(key); - - return; - } else { - level++; - } - - // Else we recurse into the correct quads - if (collidingWithTopLeft) block = topLeftBlock; - - if (collidingWithTopRight) block = topRightBlock; - - if (collidingWithBottomLeft) block = bottomLeftBlock; - - if (collidingWithBottomRight) block = bottomRightBlock; - } -} - -function getNodesInAxisAlignedRectangleArea( - maxLevel: number, - data: Float32Array, - containers: Record, - x1: number, - y1: number, - w: number, - h: number, -): string[] { - // [block, level] - const stack = [0, 0]; - - const collectedNodes: string[] = []; - - let container; - - while (stack.length) { - const level = stack.pop() as number, - block = stack.pop() as number; - - // Collecting nodes - container = containers[block]; - - if (container) extend(collectedNodes, container); - - // If we reached max level - if (level >= maxLevel) continue; - - const topLeftBlock = 4 * block + BLOCKS, - topRightBlock = 4 * block + 2 * BLOCKS, - bottomLeftBlock = 4 * block + 3 * BLOCKS, - bottomRightBlock = 4 * block + 4 * BLOCKS; - - const collidingWithTopLeft = rectangleCollidesWithQuad( - x1, - y1, - w, - h, - data[topLeftBlock + X_OFFSET], - data[topLeftBlock + Y_OFFSET], - data[topLeftBlock + WIDTH_OFFSET], - data[topLeftBlock + HEIGHT_OFFSET], - ); - - const collidingWithTopRight = rectangleCollidesWithQuad( - x1, - y1, - w, - h, - data[topRightBlock + X_OFFSET], - data[topRightBlock + Y_OFFSET], - data[topRightBlock + WIDTH_OFFSET], - data[topRightBlock + HEIGHT_OFFSET], - ); - - const collidingWithBottomLeft = rectangleCollidesWithQuad( - x1, - y1, - w, - h, - data[bottomLeftBlock + X_OFFSET], - data[bottomLeftBlock + Y_OFFSET], - data[bottomLeftBlock + WIDTH_OFFSET], - data[bottomLeftBlock + HEIGHT_OFFSET], - ); - - const collidingWithBottomRight = rectangleCollidesWithQuad( - x1, - y1, - w, - h, - data[bottomRightBlock + X_OFFSET], - data[bottomRightBlock + Y_OFFSET], - data[bottomRightBlock + WIDTH_OFFSET], - data[bottomRightBlock + HEIGHT_OFFSET], - ); - - if (collidingWithTopLeft) stack.push(topLeftBlock, level + 1); - - if (collidingWithTopRight) stack.push(topRightBlock, level + 1); - - if (collidingWithBottomLeft) stack.push(bottomLeftBlock, level + 1); - - if (collidingWithBottomRight) stack.push(bottomRightBlock, level + 1); - } - - return collectedNodes; -} - -/** - * QuadTree class. - * - * @constructor - * @param {object} boundaries - The graph boundaries. - */ -export default class QuadTree { - data: Float32Array; - containers: Record = { [OUTSIDE_BLOCK]: [] }; - cache: string[] | null = null; - lastRectangle: Rectangle | null = null; - - constructor(params: { boundaries?: Boundaries } = {}) { - // Allocating the underlying byte array - const L = Math.pow(4, MAX_LEVEL); - this.data = new Float32Array(BLOCKS * ((4 * L - 1) / 3)); - - if (params.boundaries) this.resize(params.boundaries); - else - this.resize({ - x: 0, - y: 0, - width: 1, - height: 1, - }); - } - - add(key: string, x: number, y: number, size: number): QuadTree { - insertNode(MAX_LEVEL, this.data, this.containers, key, x, y, size); - - return this; - } - - resize(boundaries: Boundaries): void { - this.clear(); - - // Building the quadrants - this.data[X_OFFSET] = boundaries.x; - this.data[Y_OFFSET] = boundaries.y; - this.data[WIDTH_OFFSET] = boundaries.width; - this.data[HEIGHT_OFFSET] = boundaries.height; - - buildQuadrants(MAX_LEVEL, this.data); - } - - clear(): QuadTree { - this.containers = { [OUTSIDE_BLOCK]: [] }; - - return this; - } - - point(x: number, y: number): string[] { - const nodes = this.containers[OUTSIDE_BLOCK].slice(); - - let block = 0, - level = 0; - - do { - if (this.containers[block]) extend(nodes, this.containers[block]); - - const quad = pointIsInQuad( - x, - y, - this.data[block + X_OFFSET], - this.data[block + Y_OFFSET], - this.data[block + WIDTH_OFFSET], - this.data[block + HEIGHT_OFFSET], - ); - - block = 4 * block + quad * BLOCKS; - level++; - } while (level <= MAX_LEVEL); - - return nodes; - } - - rectangle(x1: number, y1: number, x2: number, y2: number, height: number): string[] { - const lr = this.lastRectangle; - - if (lr && x1 === lr.x1 && x2 === lr.x2 && y1 === lr.y1 && y2 === lr.y2 && height === lr.height) { - return this.cache as string[]; - } - - this.lastRectangle = { - x1, - y1, - x2, - y2, - height, - }; - - // If the rectangle is shifted, we use the smallest aligned rectangle that contains the shifted one: - if (!isRectangleAligned(this.lastRectangle)) - this.lastRectangle = getCircumscribedAlignedRectangle(this.lastRectangle); - - this.cache = getNodesInAxisAlignedRectangleArea( - MAX_LEVEL, - this.data, - this.containers, - x1, - y1, - Math.abs(x1 - x2) || Math.abs(y1 - y2), - height, - ); - - // Add all the nodes in the outside block, since they might be relevant, and since they should be very few: - extend(this.cache, this.containers[OUTSIDE_BLOCK]); - - return this.cache; - } -} diff --git a/src/index-bundle.ts b/src/index-bundle.ts index dc60ad591..b9873f014 100644 --- a/src/index-bundle.ts +++ b/src/index-bundle.ts @@ -9,12 +9,10 @@ */ import SigmaClass from "./sigma"; import Camera from "./core/camera"; -import QuadTree from "./core/quadtree"; import MouseCaptor from "./core/captors/mouse"; class Sigma extends SigmaClass { static Camera = Camera; - static QuadTree = QuadTree; static MouseCaptor = MouseCaptor; static Sigma = SigmaClass; } diff --git a/src/index.ts b/src/index.ts index bf9e42956..ed7144376 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,8 +7,7 @@ */ import Sigma from "./sigma"; import Camera from "./core/camera"; -import QuadTree from "./core/quadtree"; import MouseCaptor from "./core/captors/mouse"; export default Sigma; -export { Camera, QuadTree, MouseCaptor, Sigma }; +export { Camera, MouseCaptor, Sigma }; diff --git a/src/rendering/webgl/programs/common/edge.ts b/src/rendering/webgl/programs/common/edge.ts index 8c2baafce..1f5aa3044 100644 --- a/src/rendering/webgl/programs/common/edge.ts +++ b/src/rendering/webgl/programs/common/edge.ts @@ -7,9 +7,11 @@ import Sigma from "../../../../sigma"; import { AbstractProgram, Program } from "./program"; import { NodeDisplayData, EdgeDisplayData, RenderParams } from "../../../../types"; +import { indexToColor } from "../../../../utils"; export abstract class AbstractEdgeProgram extends AbstractProgram { abstract process( + edgeIndex: number, offset: number, sourceData: NodeDisplayData, targetData: NodeDisplayData, @@ -21,7 +23,13 @@ export abstract class EdgeProgram extends Program implements AbstractEdgeProgram { - process(offset: number, sourceData: NodeDisplayData, targetData: NodeDisplayData, data: EdgeDisplayData): void { + process( + edgeIndex: number, + offset: number, + sourceData: NodeDisplayData, + targetData: NodeDisplayData, + data: EdgeDisplayData, + ): void { let i = offset * this.STRIDE; // NOTE: dealing with hidden items automatically if (data.hidden || sourceData.hidden || targetData.hidden) { @@ -31,10 +39,11 @@ export abstract class EdgeProgram return; } - return this.processVisibleItem(i, sourceData, targetData, data); + return this.processVisibleItem(indexToColor(edgeIndex), i, sourceData, targetData, data); } abstract processVisibleItem( - i: number, + edgeIndex: number, + startIndex: number, sourceData: NodeDisplayData, targetData: NodeDisplayData, data: EdgeDisplayData, @@ -42,7 +51,7 @@ export abstract class EdgeProgram } export interface EdgeProgramConstructor { - new (gl: WebGLRenderingContext, renderer: Sigma): AbstractEdgeProgram; + new (gl: WebGLRenderingContext, pickGl: WebGLRenderingContext | null, renderer: Sigma): AbstractEdgeProgram; } /** @@ -57,9 +66,9 @@ export function createEdgeCompoundProgram(programClasses: Array; - constructor(gl: WebGLRenderingContext, renderer: Sigma) { + constructor(gl: WebGLRenderingContext, pickGl: WebGLRenderingContext | null, renderer: Sigma) { this.programs = programClasses.map((Program) => { - return new Program(gl, renderer); + return new Program(gl, pickGl, renderer); }); } @@ -67,8 +76,14 @@ export function createEdgeCompoundProgram(programClasses: Array program.reallocate(capacity)); } - process(offset: number, sourceData: NodeDisplayData, targetData: NodeDisplayData, data: EdgeDisplayData): void { - this.programs.forEach((program) => program.process(offset, sourceData, targetData, data)); + process( + edgeIndex: number, + offset: number, + sourceData: NodeDisplayData, + targetData: NodeDisplayData, + data: EdgeDisplayData, + ): void { + this.programs.forEach((program) => program.process(edgeIndex, offset, sourceData, targetData, data)); } render(params: RenderParams): void { diff --git a/src/rendering/webgl/programs/common/node.ts b/src/rendering/webgl/programs/common/node.ts index c85219dc7..8de1e9e90 100644 --- a/src/rendering/webgl/programs/common/node.ts +++ b/src/rendering/webgl/programs/common/node.ts @@ -7,16 +7,17 @@ import Sigma from "../../../../sigma"; import { AbstractProgram, Program } from "./program"; import { NodeDisplayData, RenderParams } from "../../../../types"; +import { indexToColor } from "../../../../utils"; export abstract class AbstractNodeProgram extends AbstractProgram { - abstract process(offset: number, data: NodeDisplayData): void; + abstract process(nodeIndex: number, offset: number, data: NodeDisplayData): void; } export abstract class NodeProgram extends Program implements AbstractNodeProgram { - process(offset: number, data: NodeDisplayData): void { + process(nodeIndex: number, offset: number, data: NodeDisplayData): void { let i = offset * this.STRIDE; // NOTE: dealing with hidden items automatically if (data.hidden) { @@ -26,13 +27,13 @@ export abstract class NodeProgram return; } - return this.processVisibleItem(i, data); + return this.processVisibleItem(indexToColor(nodeIndex), i, data); } - abstract processVisibleItem(i: number, data: NodeDisplayData): void; + abstract processVisibleItem(nodeIndex: number, i: number, data: NodeDisplayData): void; } export interface NodeProgramConstructor { - new (gl: WebGLRenderingContext, renderer: Sigma): AbstractNodeProgram; + new (gl: WebGLRenderingContext, pickGl: WebGLRenderingContext | null, renderer: Sigma): AbstractNodeProgram; } /** @@ -47,9 +48,9 @@ export function createNodeCompoundProgram(programClasses: Array; - constructor(gl: WebGLRenderingContext, renderer: Sigma) { + constructor(gl: WebGLRenderingContext, pickGl: WebGLRenderingContext | null, renderer: Sigma) { this.programs = programClasses.map((Program) => { - return new Program(gl, renderer); + return new Program(gl, pickGl, renderer); }); } @@ -57,8 +58,8 @@ export function createNodeCompoundProgram(programClasses: Array program.reallocate(capacity)); } - process(offset: number, data: NodeDisplayData): void { - this.programs.forEach((program) => program.process(offset, data)); + process(nodeIndex: number, offset: number, data: NodeDisplayData): void { + this.programs.forEach((program) => program.process(nodeIndex, offset, data)); } render(params: RenderParams): void { diff --git a/src/rendering/webgl/programs/common/program.ts b/src/rendering/webgl/programs/common/program.ts index ea48c2bbe..81418c4df 100644 --- a/src/rendering/webgl/programs/common/program.ts +++ b/src/rendering/webgl/programs/common/program.ts @@ -9,6 +9,8 @@ import type Sigma from "../../../../sigma"; import type { RenderParams } from "../../../../types"; import { loadVertexShader, loadFragmentShader, loadProgram } from "../../shaders/utils"; +const PICKING_PREFIX = `#define PICKING_MODE\n`; + const SIZE_FACTOR_PER_ATTRIBUTE_TYPE: Record = { [WebGL2RenderingContext.BOOL]: 1, [WebGL2RenderingContext.BYTE]: 1, @@ -29,6 +31,16 @@ function getAttributesItemsCount(attrs: ProgramAttributeSpecification[]): number return res; } +export interface ProgramInfo { + name: string; + program: WebGLProgram; + gl: WebGLRenderingContext | WebGL2RenderingContext; + buffer: WebGLBuffer; + constantBuffer: WebGLBuffer; + uniformLocations: Record; + attributeLocations: Record; +} + export interface ProgramAttributeSpecification { name: string; size: number; @@ -42,6 +54,7 @@ export interface ProgramDefinition { FRAGMENT_SHADER_SOURCE: string; UNIFORMS: ReadonlyArray; ATTRIBUTES: Array; + METHOD: GLenum; } export interface InstancedProgramDefinition extends ProgramDefinition { @@ -51,201 +64,242 @@ export interface InstancedProgramDefinition ext export abstract class AbstractProgram { // eslint-disable-next-line @typescript-eslint/no-empty-function - constructor(_gl: WebGLRenderingContext, _renderer: Sigma) {} + constructor(_gl: WebGLRenderingContext, _pickGl: WebGLRenderingContext, _renderer: Sigma) {} abstract reallocate(capacity: number): void; abstract render(params: RenderParams): void; } -export abstract class Program implements AbstractProgram, ProgramDefinition { +export abstract class Program implements AbstractProgram, InstancedProgramDefinition { VERTICES: number; VERTEX_SHADER_SOURCE: string; FRAGMENT_SHADER_SOURCE: string; UNIFORMS: ReadonlyArray; ATTRIBUTES: Array; - ATTRIBUTES_ITEMS_COUNT: number; + METHOD: GLenum; CONSTANT_ATTRIBUTES: Array; - CONSTANT_DATA: Float32Array = new Float32Array(); + CONSTANT_DATA: number[][]; + + ATTRIBUTES_ITEMS_COUNT: number; STRIDE: number; renderer: Sigma; - gl: WebGLRenderingContext | WebGL2RenderingContext; - buffer: WebGLBuffer; array: Float32Array = new Float32Array(); - vertexShader: WebGLShader; - fragmentShader: WebGLShader; - program: WebGLProgram; - uniformLocations = {} as Record; - attributeLocations: Record = {}; + constantArray: Float32Array = new Float32Array(); capacity = 0; verticesCount = 0; + normalProgram: ProgramInfo; + pickProgram: ProgramInfo | null; + isInstanced: boolean; - constantBuffer: WebGLBuffer = {} as WebGLBuffer; abstract getDefinition(): ProgramDefinition | InstancedProgramDefinition; - constructor(gl: WebGLRenderingContext | WebGL2RenderingContext, renderer: Sigma) { - // Reading program definition - const definition = this.getDefinition(); - - this.isInstanced = false; - - this.VERTICES = definition.VERTICES; - this.VERTEX_SHADER_SOURCE = definition.VERTEX_SHADER_SOURCE; - this.FRAGMENT_SHADER_SOURCE = definition.FRAGMENT_SHADER_SOURCE; - this.UNIFORMS = definition.UNIFORMS; - this.ATTRIBUTES = definition.ATTRIBUTES; - this.CONSTANT_ATTRIBUTES = []; - this.CONSTANT_DATA = new Float32Array(); + constructor( + gl: WebGLRenderingContext | WebGL2RenderingContext, + pickGl: WebGLRenderingContext | WebGL2RenderingContext | null, + renderer: Sigma, + ) { + // Reading and caching program definition + const def = this.getDefinition(); + this.VERTICES = def.VERTICES; + this.VERTEX_SHADER_SOURCE = def.VERTEX_SHADER_SOURCE; + this.FRAGMENT_SHADER_SOURCE = def.FRAGMENT_SHADER_SOURCE; + this.UNIFORMS = def.UNIFORMS; + this.ATTRIBUTES = def.ATTRIBUTES; + this.METHOD = def.METHOD; + this.CONSTANT_ATTRIBUTES = "CONSTANT_ATTRIBUTES" in def ? def.CONSTANT_ATTRIBUTES : []; + this.CONSTANT_DATA = "CONSTANT_DATA" in def ? def.CONSTANT_DATA : []; + + this.isInstanced = "CONSTANT_ATTRIBUTES" in def; // Computing stride this.ATTRIBUTES_ITEMS_COUNT = getAttributesItemsCount(this.ATTRIBUTES); this.STRIDE = this.VERTICES * this.ATTRIBUTES_ITEMS_COUNT; // Members - this.gl = gl; this.renderer = renderer; - - // Webgl buffers - const buffer = gl.createBuffer(); - if (buffer === null) throw new Error("Program: error while creating the webgl buffer."); - this.buffer = buffer; - - // Shaders and program - this.vertexShader = loadVertexShader(this.gl, this.VERTEX_SHADER_SOURCE); - this.fragmentShader = loadFragmentShader(this.gl, this.FRAGMENT_SHADER_SOURCE); - this.program = loadProgram(this.gl, [this.vertexShader, this.fragmentShader]); - - // Initializing locations - this.UNIFORMS.forEach((uniformName) => { - const location = this.gl.getUniformLocation(this.program, uniformName); - if (location === null) throw new Error(`Program: error while getting location for uniform "${uniformName}".`); - this.uniformLocations[uniformName] = location; - }); - - this.ATTRIBUTES.forEach((attr) => { - const location = this.gl.getAttribLocation(this.program, attr.name); - if (location === -1) throw new Error(`Program: error while getting location for attribute "${attr.name}".`); - this.attributeLocations[attr.name] = location; - }); + this.normalProgram = this.getProgramInfo("normal", gl, def.VERTEX_SHADER_SOURCE, def.FRAGMENT_SHADER_SOURCE); + this.pickProgram = pickGl + ? this.getProgramInfo( + "pick", + pickGl, + PICKING_PREFIX + def.VERTEX_SHADER_SOURCE, + PICKING_PREFIX + def.FRAGMENT_SHADER_SOURCE, + ) + : null; // For instanced programs: - if ("CONSTANT_ATTRIBUTES" in definition) { - this.isInstanced = true; + if (this.isInstanced) { + const constantAttributesItemsCount = getAttributesItemsCount(this.CONSTANT_ATTRIBUTES); - const constantAttributesItemsCount = getAttributesItemsCount(definition.CONSTANT_ATTRIBUTES); - - if (definition.CONSTANT_DATA.length !== this.VERTICES) + if (this.CONSTANT_DATA.length !== this.VERTICES) throw new Error( - `Program: error while getting constant data (expected ${this.VERTICES} items, received ${definition.CONSTANT_DATA.length} instead)`, + `Program: error while getting constant data (expected ${this.VERTICES} items, received ${this.CONSTANT_DATA.length} instead)`, ); - const constantData: number[] = []; - for (let i = 0; i < definition.CONSTANT_DATA.length; i++) { - const vector = definition.CONSTANT_DATA[i]; + this.constantArray = new Float32Array(this.CONSTANT_DATA.length * constantAttributesItemsCount); + for (let i = 0; i < this.CONSTANT_DATA.length; i++) { + const vector = this.CONSTANT_DATA[i]; if (vector.length !== constantAttributesItemsCount) throw new Error( `Program: error while getting constant data (one vector has ${vector.length} items instead of ${constantAttributesItemsCount})`, ); - constantData.push(...vector); + for (let j = 0; j < vector.length; j++) this.constantArray[i * constantAttributesItemsCount + j] = vector[j]; } this.STRIDE = this.ATTRIBUTES_ITEMS_COUNT; - this.CONSTANT_DATA = new Float32Array(constantData); - this.CONSTANT_ATTRIBUTES = definition.CONSTANT_ATTRIBUTES; - this.CONSTANT_ATTRIBUTES.forEach((attr) => { - const location = this.gl.getAttribLocation(this.program, attr.name); - if (location === -1) - throw new Error(`Program: error while getting location for constant attribute "${attr.name}".`); - this.attributeLocations[attr.name] = location; + } + } + + private getProgramInfo( + name: string, + gl: WebGLRenderingContext | WebGL2RenderingContext, + vertexShaderSource: string, + fragmentShaderSource: string, + ): ProgramInfo { + const def = this.getDefinition(); + + // WebGL buffers + const buffer = gl.createBuffer(); + if (buffer === null) throw new Error("Program: error while creating the WebGL buffer."); + + // Shaders and program + const vertexShader = loadVertexShader(gl, vertexShaderSource); + const fragmentShader = loadFragmentShader(gl, fragmentShaderSource); + const program = loadProgram(gl, [vertexShader, fragmentShader]); + + // Initializing locations + const uniformLocations = {} as ProgramInfo["uniformLocations"]; + def.UNIFORMS.forEach((uniformName) => { + const location = gl.getUniformLocation(program, uniformName); + if (location) uniformLocations[uniformName] = location; + }); + + const attributeLocations = {} as ProgramInfo["attributeLocations"]; + def.ATTRIBUTES.forEach((attr) => { + attributeLocations[attr.name] = gl.getAttribLocation(program, attr.name); + }); + + // For instanced programs: + let constantBuffer; + if ("CONSTANT_ATTRIBUTES" in def) { + def.CONSTANT_ATTRIBUTES.forEach((attr) => { + attributeLocations[attr.name] = gl.getAttribLocation(program, attr.name); }); - const constantBuffer = gl.createBuffer(); - if (constantBuffer === null) throw new Error("Program: error while creating the webgl constant buffer."); - this.constantBuffer = constantBuffer; + constantBuffer = gl.createBuffer(); + if (constantBuffer === null) throw new Error("Program: error while creating the WebGL constant buffer."); } + + return { + name, + program, + gl: gl, + buffer, + constantBuffer: constantBuffer || ({} as WebGLBuffer), + uniformLocations, + attributeLocations, + }; } - protected bind(): void { - const gl = this.gl; + private bindProgram(program: ProgramInfo): void { let offset = 0; + const { gl, buffer } = program; if (!this.isInstanced) { - gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bindBuffer(gl.ARRAY_BUFFER, buffer); offset = 0; - this.ATTRIBUTES.forEach((attr) => (offset += this.bindAttribute(attr, offset))); + this.ATTRIBUTES.forEach((attr) => (offset += this.bindAttribute(attr, program, offset))); gl.bufferData(gl.ARRAY_BUFFER, this.array, gl.DYNAMIC_DRAW); } else { // Handle constant data (things that remain unchanged for all items): - gl.bindBuffer(gl.ARRAY_BUFFER, this.constantBuffer); + gl.bindBuffer(gl.ARRAY_BUFFER, program.constantBuffer); offset = 0; - this.CONSTANT_ATTRIBUTES.forEach((attr) => (offset += this.bindAttribute(attr, offset, false))); - gl.bufferData(gl.ARRAY_BUFFER, this.CONSTANT_DATA, gl.STATIC_DRAW); + this.CONSTANT_ATTRIBUTES.forEach((attr) => (offset += this.bindAttribute(attr, program, offset, false))); + gl.bufferData(gl.ARRAY_BUFFER, this.constantArray, gl.STATIC_DRAW); // Handle "instance specific" data (things that vary for each item): - gl.bindBuffer(gl.ARRAY_BUFFER, this.buffer); + gl.bindBuffer(gl.ARRAY_BUFFER, program.buffer); offset = 0; - this.ATTRIBUTES.forEach((attr) => (offset += this.bindAttribute(attr, offset, true))); + this.ATTRIBUTES.forEach((attr) => (offset += this.bindAttribute(attr, program, offset, true))); gl.bufferData(gl.ARRAY_BUFFER, this.array, gl.DYNAMIC_DRAW); } gl.bindBuffer(gl.ARRAY_BUFFER, null); } + protected bind(): void { + this.bindProgram(this.normalProgram); + if (this.pickProgram) this.bindProgram(this.pickProgram); + } - protected unbind(): void { + private unbindProgram(program: ProgramInfo): void { if (!this.isInstanced) { - this.ATTRIBUTES.forEach((attr) => this.unbindAttribute(attr)); + this.ATTRIBUTES.forEach((attr) => this.unbindAttribute(attr, program)); } else { - this.CONSTANT_ATTRIBUTES.forEach((attr) => this.unbindAttribute(attr, false)); - this.ATTRIBUTES.forEach((attr) => this.unbindAttribute(attr, true)); + this.CONSTANT_ATTRIBUTES.forEach((attr) => this.unbindAttribute(attr, program, false)); + this.ATTRIBUTES.forEach((attr) => this.unbindAttribute(attr, program, true)); } } + protected unbind(): void { + this.unbindProgram(this.normalProgram); + if (this.pickProgram) this.unbindProgram(this.pickProgram); + } - private bindAttribute(attr: ProgramAttributeSpecification, offset: number, setDivisor?: boolean): number { - const gl = this.gl; - const location = this.attributeLocations[attr.name]; + private bindAttribute( + attr: ProgramAttributeSpecification, + program: ProgramInfo, + offset: number, + setDivisor?: boolean, + ): number { + const sizeFactor = SIZE_FACTOR_PER_ATTRIBUTE_TYPE[attr.type]; + if (typeof sizeFactor !== "number") throw new Error(`Program.bind: yet unsupported attribute type "${attr.type}"!`); - gl.enableVertexAttribArray(location); + const location = program.attributeLocations[attr.name]; + const gl = program.gl; - const stride = !this.isInstanced - ? this.ATTRIBUTES_ITEMS_COUNT * Float32Array.BYTES_PER_ELEMENT - : (setDivisor ? this.ATTRIBUTES_ITEMS_COUNT : getAttributesItemsCount(this.CONSTANT_ATTRIBUTES)) * - Float32Array.BYTES_PER_ELEMENT; + if (location !== -1) { + gl.enableVertexAttribArray(location); - gl.vertexAttribPointer(location, attr.size, attr.type, attr.normalized || false, stride, offset); + const stride = !this.isInstanced + ? this.ATTRIBUTES_ITEMS_COUNT * Float32Array.BYTES_PER_ELEMENT + : (setDivisor ? this.ATTRIBUTES_ITEMS_COUNT : getAttributesItemsCount(this.CONSTANT_ATTRIBUTES)) * + Float32Array.BYTES_PER_ELEMENT; - if (this.isInstanced && setDivisor) { - if (gl instanceof WebGL2RenderingContext) { - gl.vertexAttribDivisor(location, 1); - } else { - const ext = gl.getExtension("ANGLE_instanced_arrays"); - if (ext) ext.vertexAttribDivisorANGLE(location, 1); + gl.vertexAttribPointer(location, attr.size, attr.type, attr.normalized || false, stride, offset); + + if (this.isInstanced && setDivisor) { + if (gl instanceof WebGL2RenderingContext) { + gl.vertexAttribDivisor(location, 1); + } else { + const ext = gl.getExtension("ANGLE_instanced_arrays"); + if (ext) ext.vertexAttribDivisorANGLE(location, 1); + } } } - const sizeFactor = SIZE_FACTOR_PER_ATTRIBUTE_TYPE[attr.type]; - if (typeof sizeFactor !== "number") throw new Error(`Program.bind: yet unsupported attribute type "${attr.type}"!`); - return attr.size * sizeFactor; } - private unbindAttribute(attr: ProgramAttributeSpecification, unsetDivisor?: boolean) { - const gl = this.gl; - const location = this.attributeLocations[attr.name]; + private unbindAttribute(attr: ProgramAttributeSpecification, program: ProgramInfo, unsetDivisor?: boolean) { + const location = program.attributeLocations[attr.name]; + const gl = program.gl; - gl.disableVertexAttribArray(location); + if (location !== -1) { + gl.disableVertexAttribArray(location); - if (this.isInstanced && unsetDivisor) { - if (gl instanceof WebGL2RenderingContext) { - gl.vertexAttribDivisor(location, 0); - } else { - const ext = gl.getExtension("ANGLE_instanced_arrays"); - if (ext) ext.vertexAttribDivisorANGLE(location, 0); + if (this.isInstanced && unsetDivisor) { + if (gl instanceof WebGL2RenderingContext) { + gl.vertexAttribDivisor(location, 0); + } else { + const ext = gl.getExtension("ANGLE_instanced_arrays"); + if (ext) ext.vertexAttribDivisorANGLE(location, 0); + } } } } @@ -269,20 +323,24 @@ export abstract class Program implements Abstra return this.verticesCount === 0; } - abstract draw(params: RenderParams): void; + abstract setUniforms(params: RenderParams, programInfo: ProgramInfo): void; + private renderProgram(params: RenderParams, programInfo: ProgramInfo): void { + const { gl, program } = programInfo; + gl.useProgram(program); + this.setUniforms(params, programInfo); + this.drawWebGL(this.METHOD, programInfo); + } render(params: RenderParams): void { if (this.hasNothingToRender()) return; this.bind(); - this.gl.useProgram(this.program); - this.draw(params); + this.renderProgram(params, this.normalProgram); + if (this.pickProgram) this.renderProgram(params, this.pickProgram); this.unbind(); } - drawWebGL(method: GLenum): void { - const gl = this.gl; - + drawWebGL(method: GLenum, { gl }: ProgramInfo): void { if (!this.isInstanced) { gl.drawArrays(method, 0, this.verticesCount); } else { diff --git a/src/rendering/webgl/programs/edge.arrowHead.ts b/src/rendering/webgl/programs/edge.arrowHead.ts index 27d0bafae..f8e3e297b 100644 --- a/src/rendering/webgl/programs/edge.arrowHead.ts +++ b/src/rendering/webgl/programs/edge.arrowHead.ts @@ -10,23 +10,26 @@ import { floatColor } from "../../../utils"; import { EdgeProgram } from "./common/edge"; import VERTEX_SHADER_SOURCE from "../shaders/edge.arrowHead.vert.glsl"; import FRAGMENT_SHADER_SOURCE from "../shaders/edge.arrowHead.frag.glsl"; +import { ProgramInfo } from "./common/program"; const { UNSIGNED_BYTE, FLOAT } = WebGLRenderingContext; const UNIFORMS = ["u_matrix", "u_sizeRatio", "u_correctionRatio"] as const; -export default class EdgeArrowHeadProgram extends EdgeProgram { +export default class EdgeArrowHeadProgram extends EdgeProgram<(typeof UNIFORMS)[number]> { getDefinition() { return { VERTICES: 3, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE, + METHOD: WebGLRenderingContext.TRIANGLES, UNIFORMS, ATTRIBUTES: [ { name: "a_position", size: 2, type: FLOAT }, { name: "a_normal", size: 2, type: FLOAT }, { name: "a_radius", size: 1, type: FLOAT }, { name: "a_color", size: 4, type: UNSIGNED_BYTE, normalized: true }, + { name: "a_id", size: 4, type: UNSIGNED_BYTE, normalized: true }, ], CONSTANT_ATTRIBUTES: [{ name: "a_barycentric", size: 3, type: FLOAT }], CONSTANT_DATA: [ @@ -37,7 +40,13 @@ export default class EdgeArrowHeadProgram extends EdgeProgram { +export default class EdgeLineProgram extends EdgeProgram<(typeof UNIFORMS)[number]> { getDefinition() { return { VERTICES: 2, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE, + METHOD: WebGLRenderingContext.LINES, UNIFORMS, ATTRIBUTES: [ { name: "a_position", size: 2, type: FLOAT }, { name: "a_color", size: 4, type: UNSIGNED_BYTE, normalized: true }, + { name: "a_id", size: 4, type: UNSIGNED_BYTE, normalized: true }, ], }; } - processVisibleItem(i: number, sourceData: NodeDisplayData, targetData: NodeDisplayData, data: EdgeDisplayData) { + processVisibleItem( + edgeIndex: number, + startIndex: number, + sourceData: NodeDisplayData, + targetData: NodeDisplayData, + data: EdgeDisplayData, + ) { const array = this.array; const x1 = sourceData.x; @@ -40,23 +49,21 @@ export default class EdgeLineProgram extends EdgeProgram { +export default class EdgeRectangleProgram extends EdgeProgram<(typeof UNIFORMS)[number]> { getDefinition() { return { VERTICES: 6, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE, + METHOD: WebGLRenderingContext.TRIANGLES, UNIFORMS, ATTRIBUTES: [ { name: "a_positionStart", size: 2, type: FLOAT }, { name: "a_positionEnd", size: 2, type: FLOAT }, { name: "a_normal", size: 2, type: FLOAT }, { name: "a_color", size: 4, type: UNSIGNED_BYTE, normalized: true }, + { name: "a_id", size: 4, type: UNSIGNED_BYTE, normalized: true }, ], CONSTANT_ATTRIBUTES: [ // If 0, then position will be a_positionStart @@ -55,7 +58,13 @@ export default class EdgeRectangleProgram extends EdgeProgram { +export default class EdgeTriangleProgram extends EdgeProgram<(typeof UNIFORMS)[number]> { getDefinition() { return { VERTICES: 3, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE, + METHOD: WebGLRenderingContext.TRIANGLES, UNIFORMS, ATTRIBUTES: [ { name: "a_positionStart", size: 2, type: FLOAT }, { name: "a_positionEnd", size: 2, type: FLOAT }, { name: "a_normal", size: 2, type: FLOAT }, { name: "a_color", size: 4, type: UNSIGNED_BYTE, normalized: true }, + { name: "a_id", size: 4, type: UNSIGNED_BYTE, normalized: true }, ], CONSTANT_ATTRIBUTES: [ // If 0, then position will be a_positionStart @@ -42,7 +45,13 @@ export default class EdgeTriangleProgram extends EdgeProgram { +export default class NodeCircleProgram extends NodeProgram<(typeof UNIFORMS)[number]> { static readonly ANGLE_1 = 0; static readonly ANGLE_2 = (2 * Math.PI) / 3; static readonly ANGLE_3 = (4 * Math.PI) / 3; @@ -28,36 +29,35 @@ export default class NodeCircleProgram extends NodeProgram { + return class NodeImageProgram extends NodeProgram<(typeof UNIFORMS)[number]> { getDefinition() { return { VERTICES: 1, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE, + METHOD: WebGLRenderingContext.POINTS, UNIFORMS, ATTRIBUTES: [ { name: "a_position", size: 2, type: FLOAT }, { name: "a_size", size: 1, type: FLOAT }, { name: "a_color", size: 4, type: UNSIGNED_BYTE, normalized: true }, + { name: "a_id", size: 4, type: UNSIGNED_BYTE, normalized: true }, { name: "a_texture", size: 4, type: FLOAT }, ], }; @@ -219,8 +222,8 @@ export default function getNodeImageProgram(): NodeProgramConstructor { texture: WebGLTexture; latestRenderParams?: RenderParams; - constructor(gl: WebGLRenderingContext, renderer: Sigma) { - super(gl, renderer); + constructor(gl: WebGLRenderingContext, pickGl: WebGLRenderingContext | null, renderer: Sigma) { + super(gl, pickGl, renderer); rebindTextureFns.push(() => { if (this && this.rebindTexture) this.rebindTexture(); @@ -235,7 +238,7 @@ export default function getNodeImageProgram(): NodeProgramConstructor { } rebindTexture() { - const gl = this.gl; + const gl = this.normalProgram.gl; gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImage); @@ -244,46 +247,43 @@ export default function getNodeImageProgram(): NodeProgramConstructor { if (this.latestRenderParams) this.render(this.latestRenderParams); } - processVisibleItem(i: number, data: NodeDisplayData & { image?: string }): void { + processVisibleItem(nodeIndex: number, startIndex: number, data: NodeDisplayData & { image?: string }): void { const array = this.array; const imageSource = data.image; const imageState = imageSource && images[imageSource]; if (typeof imageSource === "string" && !imageState) loadImage(imageSource); - array[i++] = data.x; - array[i++] = data.y; - array[i++] = data.size; - array[i++] = floatColor(data.color); + array[startIndex++] = data.x; + array[startIndex++] = data.y; + array[startIndex++] = data.size; + array[startIndex++] = floatColor(data.color); + array[startIndex++] = nodeIndex; // Reference texture: if (imageState && imageState.status === "ready") { const { width, height } = textureImage; - array[i++] = imageState.x / width; - array[i++] = imageState.y / height; - array[i++] = imageState.width / width; - array[i++] = imageState.height / height; + array[startIndex++] = imageState.x / width; + array[startIndex++] = imageState.y / height; + array[startIndex++] = imageState.width / width; + array[startIndex++] = imageState.height / height; } else { - array[i++] = 0; - array[i++] = 0; - array[i++] = 0; - array[i++] = 0; + array[startIndex++] = 0; + array[startIndex++] = 0; + array[startIndex++] = 0; + array[startIndex++] = 0; } } - draw(params: RenderParams): void { + setUniforms(params: RenderParams, { gl, uniformLocations }: ProgramInfo): void { this.latestRenderParams = params; - const gl = this.gl; - - const { u_sizeRatio, u_pixelRatio, u_matrix, u_atlas } = this.uniformLocations; + const { u_sizeRatio, u_pixelRatio, u_matrix, u_atlas } = uniformLocations; gl.uniform1f(u_sizeRatio, params.sizeRatio); gl.uniform1f(u_pixelRatio, params.pixelRatio); gl.uniformMatrix3fv(u_matrix, false, params.matrix); gl.uniform1i(u_atlas, 0); - - this.drawWebGL(gl.POINTS); } }; } diff --git a/src/rendering/webgl/programs/node.point.ts b/src/rendering/webgl/programs/node.point.ts index adb7b3e4d..7e66d4283 100644 --- a/src/rendering/webgl/programs/node.point.ts +++ b/src/rendering/webgl/programs/node.point.ts @@ -12,44 +12,44 @@ import { floatColor } from "../../../utils"; import { NodeProgram } from "./common/node"; import VERTEX_SHADER_SOURCE from "../shaders/node.point.vert.glsl"; import FRAGMENT_SHADER_SOURCE from "../shaders/node.point.frag.glsl"; +import { ProgramInfo } from "./common/program"; const { UNSIGNED_BYTE, FLOAT } = WebGLRenderingContext; const UNIFORMS = ["u_sizeRatio", "u_pixelRatio", "u_matrix"] as const; -export default class NodePointProgram extends NodeProgram { +export default class NodePointProgram extends NodeProgram<(typeof UNIFORMS)[number]> { getDefinition() { return { VERTICES: 1, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE, + METHOD: WebGLRenderingContext.POINTS, UNIFORMS, ATTRIBUTES: [ { name: "a_position", size: 2, type: FLOAT }, { name: "a_size", size: 1, type: FLOAT }, { name: "a_color", size: 4, type: UNSIGNED_BYTE, normalized: true }, + { name: "a_id", size: 4, type: UNSIGNED_BYTE, normalized: true }, ], }; } - processVisibleItem(i: number, data: NodeDisplayData) { + processVisibleItem(nodeIndex: number, startIndex: number, data: NodeDisplayData) { const array = this.array; - array[i++] = data.x; - array[i++] = data.y; - array[i++] = data.size; - array[i] = floatColor(data.color); + array[startIndex++] = data.x; + array[startIndex++] = data.y; + array[startIndex++] = data.size; + array[startIndex++] = floatColor(data.color); + array[startIndex++] = nodeIndex; } - draw(params: RenderParams): void { - const gl = this.gl; - - const { u_sizeRatio, u_pixelRatio, u_matrix } = this.uniformLocations; + setUniforms(params: RenderParams, { gl, uniformLocations }: ProgramInfo): void { + const { u_sizeRatio, u_pixelRatio, u_matrix } = uniformLocations; gl.uniform1f(u_sizeRatio, params.sizeRatio); gl.uniform1f(u_pixelRatio, params.pixelRatio); gl.uniformMatrix3fv(u_matrix, false, params.matrix); - - this.drawWebGL(gl.POINTS); } } diff --git a/src/rendering/webgl/shaders/edge.arrowHead.vert.glsl b/src/rendering/webgl/shaders/edge.arrowHead.vert.glsl index f9c4ad2d2..4e4d84fa2 100644 --- a/src/rendering/webgl/shaders/edge.arrowHead.vert.glsl +++ b/src/rendering/webgl/shaders/edge.arrowHead.vert.glsl @@ -1,9 +1,14 @@ attribute vec2 a_position; attribute vec2 a_normal; attribute float a_radius; -attribute vec4 a_color; attribute vec3 a_barycentric; +#ifdef PICKING_MODE +attribute vec4 a_id; +#else +attribute vec4 a_color; +#endif + uniform mat3 u_matrix; uniform float u_sizeRatio; uniform float u_correctionRatio; @@ -46,7 +51,12 @@ void main() { gl_Position = vec4(position, 0, 1); - // Extract the color: + #ifdef PICKING_MODE + // For picking mode, we use the ID as the color: + v_color = a_id; + #else + // For normal mode, we use the color: v_color = a_color; v_color.a *= bias; + #endif } diff --git a/src/rendering/webgl/shaders/edge.clamped.vert.glsl b/src/rendering/webgl/shaders/edge.clamped.vert.glsl index b21335bb3..374f047e3 100644 --- a/src/rendering/webgl/shaders/edge.clamped.vert.glsl +++ b/src/rendering/webgl/shaders/edge.clamped.vert.glsl @@ -1,3 +1,4 @@ +attribute vec4 a_id; attribute vec4 a_color; attribute vec2 a_normal; attribute float a_normalCoef; @@ -46,6 +47,13 @@ void main() { v_thickness = webGLThickness / u_zoomRatio; v_normal = unitNormal; + + #ifdef PICKING_MODE + // For picking mode, we use the ID as the color: + v_color = a_id; + #else + // For normal mode, we use the color: v_color = a_color; v_color.a *= bias; + #endif } diff --git a/src/rendering/webgl/shaders/edge.line.vert.glsl b/src/rendering/webgl/shaders/edge.line.vert.glsl index da8df7104..62bed9bd8 100644 --- a/src/rendering/webgl/shaders/edge.line.vert.glsl +++ b/src/rendering/webgl/shaders/edge.line.vert.glsl @@ -1,5 +1,6 @@ -attribute vec2 a_position; +attribute vec4 a_id; attribute vec4 a_color; +attribute vec2 a_position; uniform mat3 u_matrix; @@ -15,7 +16,12 @@ void main() { 1 ); - // Extract the color: + #ifdef PICKING_MODE + // For picking mode, we use the ID as the color: + v_color = a_id; + #else + // For normal mode, we use the color: v_color = a_color; v_color.a *= bias; + #endif } diff --git a/src/rendering/webgl/shaders/edge.rectangle.frag.glsl b/src/rendering/webgl/shaders/edge.rectangle.frag.glsl index f384b1448..9fd563178 100644 --- a/src/rendering/webgl/shaders/edge.rectangle.frag.glsl +++ b/src/rendering/webgl/shaders/edge.rectangle.frag.glsl @@ -8,6 +8,10 @@ const float feather = 0.001; const vec4 transparent = vec4(0.0, 0.0, 0.0, 0.0); void main(void) { + // We only handle antialiasing for normal mode: + #ifdef PICKING_MODE + gl_FragColor = v_color; + #else float dist = length(v_normal) * v_thickness; float t = smoothstep( @@ -17,4 +21,5 @@ void main(void) { ); gl_FragColor = mix(v_color, transparent, t); + #endif } diff --git a/src/rendering/webgl/shaders/edge.rectangle.vert.glsl b/src/rendering/webgl/shaders/edge.rectangle.vert.glsl index e50395b65..deb2bceb4 100644 --- a/src/rendering/webgl/shaders/edge.rectangle.vert.glsl +++ b/src/rendering/webgl/shaders/edge.rectangle.vert.glsl @@ -1,3 +1,4 @@ +attribute vec4 a_id; attribute vec4 a_color; attribute vec2 a_normal; attribute float a_normalCoef; @@ -43,6 +44,13 @@ void main() { v_thickness = webGLThickness / u_zoomRatio; v_normal = unitNormal; + + #ifdef PICKING_MODE + // For picking mode, we use the ID as the color: + v_color = a_id; + #else + // For normal mode, we use the color: v_color = a_color; v_color.a *= bias; + #endif } diff --git a/src/rendering/webgl/shaders/edge.triangle.vert.glsl b/src/rendering/webgl/shaders/edge.triangle.vert.glsl index d1ee838b5..a10ad9474 100644 --- a/src/rendering/webgl/shaders/edge.triangle.vert.glsl +++ b/src/rendering/webgl/shaders/edge.triangle.vert.glsl @@ -1,3 +1,4 @@ +attribute vec4 a_id; attribute vec4 a_color; attribute vec2 a_normal; attribute float a_normalCoef; @@ -29,6 +30,12 @@ void main() { gl_Position = vec4((u_matrix * vec3(position + unitNormal * webGLThickness, 1)).xy, 0, 1); + #ifdef PICKING_MODE + // For picking mode, we use the ID as the color: + v_color = a_id; + #else + // For normal mode, we use the color: v_color = a_color; v_color.a *= bias; + #endif } diff --git a/src/rendering/webgl/shaders/node.circle.frag.glsl b/src/rendering/webgl/shaders/node.circle.frag.glsl index 67e1020ff..eabe513da 100644 --- a/src/rendering/webgl/shaders/node.circle.frag.glsl +++ b/src/rendering/webgl/shaders/node.circle.frag.glsl @@ -10,6 +10,14 @@ const vec4 transparent = vec4(0.0, 0.0, 0.0, 0.0); void main(void) { float dist = length(v_diffVector) - v_radius; + // No antialiasing for picking mode: + #ifdef PICKING_MODE + if (dist > v_border) + gl_FragColor = transparent; + else + gl_FragColor = v_color; + + #else float t = 0.0; if (dist > v_border) t = 1.0; @@ -17,4 +25,5 @@ void main(void) { t = dist / v_border; gl_FragColor = mix(v_color, transparent, t); + #endif } diff --git a/src/rendering/webgl/shaders/node.circle.vert.glsl b/src/rendering/webgl/shaders/node.circle.vert.glsl index 7689917e0..612d8d8de 100644 --- a/src/rendering/webgl/shaders/node.circle.vert.glsl +++ b/src/rendering/webgl/shaders/node.circle.vert.glsl @@ -1,7 +1,8 @@ +attribute vec4 a_id; +attribute vec4 a_color; attribute vec2 a_position; attribute float a_size; attribute float a_angle; -attribute vec4 a_color; uniform mat3 u_matrix; uniform float u_sizeRatio; @@ -29,6 +30,12 @@ void main() { v_diffVector = diffVector; v_radius = size / 2.0 / marginRatio; + #ifdef PICKING_MODE + // For picking mode, we use the ID as the color: + v_color = a_id; + #else + // For normal mode, we use the color: v_color = a_color; v_color.a *= bias; + #endif } diff --git a/src/rendering/webgl/shaders/node.image.frag.glsl b/src/rendering/webgl/shaders/node.image.frag.glsl index 702b7c1d1..042d6e2d4 100644 --- a/src/rendering/webgl/shaders/node.image.frag.glsl +++ b/src/rendering/webgl/shaders/node.image.frag.glsl @@ -10,6 +10,17 @@ const float radius = 0.5; const vec4 transparent = vec4(0.0, 0.0, 0.0, 0.0); void main(void) { + vec2 m = gl_PointCoord - vec2(0.5, 0.5); + float dist = length(m); + + // No antialiasing for picking mode: + #ifdef PICKING_MODE + if (dist > radius) + gl_FragColor = transparent; + else + gl_FragColor = v_color; + + #else vec4 color; if (v_texture.w > 0.0) { @@ -19,9 +30,6 @@ void main(void) { color = v_color; } - vec2 m = gl_PointCoord - vec2(0.5, 0.5); - float dist = length(m); - if (dist < radius - v_border) { gl_FragColor = color; } else if (dist < radius) { @@ -29,4 +37,5 @@ void main(void) { } else { gl_FragColor = transparent; } + #endif } diff --git a/src/rendering/webgl/shaders/node.image.vert.glsl b/src/rendering/webgl/shaders/node.image.vert.glsl index 08c83a67b..fb4b83e61 100644 --- a/src/rendering/webgl/shaders/node.image.vert.glsl +++ b/src/rendering/webgl/shaders/node.image.vert.glsl @@ -1,6 +1,7 @@ +attribute vec4 a_id; +attribute vec4 a_color; attribute vec2 a_position; attribute float a_size; -attribute vec4 a_color; attribute vec4 a_texture; uniform float u_sizeRatio; @@ -27,10 +28,15 @@ void main() { v_border = (0.5 / a_size) * u_sizeRatio; - // Extract the color: + #ifdef PICKING_MODE + // For picking mode, we use the ID as the color: + v_color = a_id; + #else + // For normal mode, we use the color: v_color = a_color; v_color.a *= bias; // Pass the texture coordinates: v_texture = a_texture; + #endif } diff --git a/src/rendering/webgl/shaders/node.point.frag.glsl b/src/rendering/webgl/shaders/node.point.frag.glsl index 1f1631a43..fb5837077 100644 --- a/src/rendering/webgl/shaders/node.point.frag.glsl +++ b/src/rendering/webgl/shaders/node.point.frag.glsl @@ -10,6 +10,14 @@ void main(void) { vec2 m = gl_PointCoord - vec2(0.5, 0.5); float dist = radius - length(m); + // No antialiasing for picking mode: + #ifdef PICKING_MODE + if (dist > v_border) + gl_FragColor = transparent; + else + gl_FragColor = v_color; + + #else float t = 0.0; if (dist > v_border) t = 1.0; @@ -17,4 +25,5 @@ void main(void) { t = dist / v_border; gl_FragColor = mix(transparent, v_color, t); + #endif } diff --git a/src/rendering/webgl/shaders/node.point.vert.glsl b/src/rendering/webgl/shaders/node.point.vert.glsl index 1254b1f80..fe1f3f6b8 100644 --- a/src/rendering/webgl/shaders/node.point.vert.glsl +++ b/src/rendering/webgl/shaders/node.point.vert.glsl @@ -1,6 +1,7 @@ +attribute vec4 a_id; +attribute vec4 a_color; attribute vec2 a_position; attribute float a_size; -attribute vec4 a_color; uniform float u_sizeRatio; uniform float u_pixelRatio; @@ -25,7 +26,12 @@ void main() { v_border = (0.5 / a_size) * u_sizeRatio; - // Extract the color: + #ifdef PICKING_MODE + // For picking mode, we use the ID as the color: + v_color = a_id; + #else + // For normal mode, we use the color: v_color = a_color; v_color.a *= bias; + #endif } diff --git a/src/settings.ts b/src/settings.ts index 1ae68ed23..0d369d8fc 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -17,6 +17,7 @@ import EdgeRectangleProgram from "./rendering/webgl/programs/edge.rectangle"; import EdgeArrowProgram from "./rendering/webgl/programs/edge.arrow"; import { EdgeProgramConstructor } from "./rendering/webgl/programs/common/edge"; import { NodeProgramConstructor } from "./rendering/webgl/programs/common/node"; +import NodeCircleProgram from "./rendering/webgl/programs/node.circle"; /** * Sigma.js settings @@ -127,7 +128,7 @@ export const DEFAULT_SETTINGS: Settings = { }; export const DEFAULT_NODE_PROGRAM_CLASSES = { - circle: NodePointProgram, + circle: NodeCircleProgram, }; export const DEFAULT_EDGE_PROGRAM_CLASSES = { diff --git a/src/sigma.ts b/src/sigma.ts index 416c00c0e..8118920c1 100644 --- a/src/sigma.ts +++ b/src/sigma.ts @@ -8,7 +8,6 @@ import extend from "@yomguithereal/helpers/extend"; import Camera from "./core/camera"; import MouseCaptor from "./core/captors/mouse"; -import QuadTree from "./core/quadtree"; import { CameraState, Coordinates, @@ -35,6 +34,7 @@ import { zIndexOrdering, getMatrixImpact, graphExtent, + colorToIndex, } from "./utils"; import { edgeLabelsToDisplayFromNodes, LabelGrid } from "./core/labels"; import { Settings, validateSettings, resolveSettings } from "./settings"; @@ -42,7 +42,7 @@ import { AbstractNodeProgram } from "./rendering/webgl/programs/common/node"; import { AbstractEdgeProgram } from "./rendering/webgl/programs/common/edge"; import TouchCaptor, { FakeSigmaMouseEvent } from "./core/captors/touch"; import { identity, multiplyVec2 } from "./utils/matrices"; -import { doEdgeCollideWithPoint, isPixelColored } from "./utils/edge-collisions"; +import { getPixelColor } from "./utils/edge-collisions"; /** * Constants. @@ -163,7 +163,6 @@ export default class Sigma extends TypedEventEm private canvasContexts: PlainObject = {}; private webGLContexts: PlainObject = {}; private activeListeners: PlainObject = {}; - private quadtree: QuadTree = new QuadTree(); private labelGrid: LabelGrid = new LabelGrid(); private nodeDataCache: Record = {}; private edgeDataCache: Record = {}; @@ -182,6 +181,7 @@ export default class Sigma extends TypedEventEm // Cache: private graphToViewportRatio = 1; + private itemIDsIndex: Record = {}; // Starting dimensions and pixel ratio private width = 0; @@ -222,13 +222,12 @@ export default class Sigma extends TypedEventEm this.container = container; // Initializing contexts - this.createWebGLContext("edges", { preserveDrawingBuffer: true }); + this.createWebGLContext("edges"); this.createCanvasContext("edgeLabels"); this.createWebGLContext("nodes"); this.createCanvasContext("labels"); this.createCanvasContext("hovers"); this.createWebGLContext("hoverNodes"); - this.createCanvasContext("mouse"); // Blending for (const key in this.webGLContexts) { @@ -238,21 +237,24 @@ export default class Sigma extends TypedEventEm gl.enable(gl.BLEND); } + this.createWebGLContext("picking", { preserveDrawingBuffer: true, hidden: true }); + this.createCanvasContext("mouse"); + // Loading programs for (const type in this.settings.nodeProgramClasses) { const NodeProgramClass = this.settings.nodeProgramClasses[type]; - this.nodePrograms[type] = new NodeProgramClass(this.webGLContexts.nodes, this); + this.nodePrograms[type] = new NodeProgramClass(this.webGLContexts.nodes, this.webGLContexts.picking, this); let NodeHoverProgram = NodeProgramClass; if (type in this.settings.nodeHoverProgramClasses) { NodeHoverProgram = this.settings.nodeHoverProgramClasses[type]; } - this.nodeHoverPrograms[type] = new NodeHoverProgram(this.webGLContexts.hoverNodes, this); + this.nodeHoverPrograms[type] = new NodeHoverProgram(this.webGLContexts.hoverNodes, null, this); } for (const type in this.settings.edgeProgramClasses) { const EdgeProgramClass = this.settings.edgeProgramClasses[type]; - this.edgePrograms[type] = new EdgeProgramClass(this.webGLContexts.edges, this); + this.edgePrograms[type] = new EdgeProgramClass(this.webGLContexts.edges, this.webGLContexts.picking, this); } // Initial resize @@ -336,8 +338,12 @@ export default class Sigma extends TypedEventEm * @param {object?} options - #getContext params to override (optional) * @return {Sigma} */ - private createWebGLContext(id: string, options?: { preserveDrawingBuffer?: boolean; antialias?: boolean }): this { + private createWebGLContext( + id: string, + options?: { preserveDrawingBuffer?: boolean; antialias?: boolean; hidden?: boolean }, + ): this { const canvas = this.createCanvas(id); + if (options?.hidden) canvas.remove(); const contextOptions = { preserveDrawingBuffer: false, @@ -399,47 +405,16 @@ export default class Sigma extends TypedEventEm ); } - /** - * Method that returns all nodes in quad at a given position. - */ - private getQuadNodes(position: Coordinates): string[] { - const mouseGraphPosition = this.viewportToFramedGraph(position); - - return this.quadtree.point(mouseGraphPosition.x, 1 - mouseGraphPosition.y); - } - /** * Method that returns the closest node to a given position. */ private getNodeAtPosition(position: Coordinates): string | null { const { x, y } = position; - const quadNodes = this.getQuadNodes(position); - - // We will hover the node whose center is closest to mouse - let minDistance = Infinity, - nodeAtPosition = null; + const color = getPixelColor(this.webGLContexts.picking, x, y); + const index = colorToIndex(...color); + const itemAt = this.itemIDsIndex[index]; - for (let i = 0, l = quadNodes.length; i < l; i++) { - const node = quadNodes[i]; - - const data = this.nodeDataCache[node]; - - const nodePosition = this.framedGraphToViewport(data); - - const size = this.scaleSize(data.size); - - if (!data.hidden && this.mouseIsOnNode(position, nodePosition, size)) { - const distance = Math.sqrt(Math.pow(x - nodePosition.x, 2) + Math.pow(y - nodePosition.y, 2)); - - // TODO: sort by min size also for cases where center is the same - if (distance < minDistance) { - minDistance = distance; - nodeAtPosition = node; - } - } - } - - return nodeAtPosition; + return itemAt && itemAt.type === "node" ? itemAt.id : null; } /** @@ -650,72 +625,11 @@ export default class Sigma extends TypedEventEm * the key of the edge if any, or null else. */ private getEdgeAtPoint(x: number, y: number): string | null { - const { edgeDataCache, nodeDataCache } = this; - - // Check first that pixel is colored: - // Note that mouse positions must be corrected by pixel ratio to correctly - // index the drawing buffer. - if (!isPixelColored(this.webGLContexts.edges, x * this.pixelRatio, y * this.pixelRatio)) return null; + const color = getPixelColor(this.webGLContexts.picking, x, y); + const index = colorToIndex(...color); + const itemAt = this.itemIDsIndex[index]; - // Check for each edge if it collides with the point: - const { x: graphX, y: graphY } = this.viewportToGraph({ x, y }); - - // To translate edge thicknesses to the graph system, we observe by how much - // the length of a non-null edge is transformed to between the graph system - // and the viewport system: - let transformationRatio = 0; - this.graph.someEdge((key, _, sourceId, targetId, { x: xs, y: ys }, { x: xt, y: yt }) => { - if (edgeDataCache[key].hidden || nodeDataCache[sourceId].hidden || nodeDataCache[targetId].hidden) return false; - - if (xs !== xt || ys !== yt) { - const graphLength = Math.sqrt(Math.pow(xt - xs, 2) + Math.pow(yt - ys, 2)); - - const { x: vp_xs, y: vp_ys } = this.graphToViewport({ x: xs, y: ys }); - const { x: vp_xt, y: vp_yt } = this.graphToViewport({ x: xt, y: yt }); - const viewportLength = Math.sqrt(Math.pow(vp_xt - vp_xs, 2) + Math.pow(vp_yt - vp_ys, 2)); - - transformationRatio = graphLength / viewportLength; - return true; - } - }); - // If no non-null edge has been found, return null: - if (!transformationRatio) return null; - - // Now we can look for matching edges: - const edges = this.graph.filterEdges((key, edgeAttributes, sourceId, targetId, sourcePosition, targetPosition) => { - if (edgeDataCache[key].hidden || nodeDataCache[sourceId].hidden || nodeDataCache[targetId].hidden) return false; - if ( - doEdgeCollideWithPoint( - graphX, - graphY, - sourcePosition.x, - sourcePosition.y, - targetPosition.x, - targetPosition.y, - // Adapt the edge size to the zoom ratio: - this.scaleSize(edgeDataCache[key].size * transformationRatio), - ) - ) { - return true; - } - }); - - if (edges.length === 0) return null; // no edges found - - // if none of the edges have a zIndex, selected the most recently created one to match the rendering order - let selectedEdge = edges[edges.length - 1]; - - // otherwise select edge with highest zIndex - let highestZIndex = -Infinity; - for (const edge of edges) { - const zIndex = this.graph.getEdgeAttribute(edge, "zIndex"); - if (zIndex >= highestZIndex) { - selectedEdge = edge; - highestZIndex = zIndex; - } - } - - return selectedEdge; + return itemAt && itemAt.type === "edge" ? itemAt.id : null; } /** @@ -731,9 +645,6 @@ export default class Sigma extends TypedEventEm const nodeZExtent: [number, number] = [Infinity, -Infinity]; const edgeZExtent: [number, number] = [Infinity, -Infinity]; - // Clearing the quad - this.quadtree.clear(); - // Resetting the label grid // TODO: it's probably better to do this explicitly or on resizes for layout and anims this.labelGrid.resizeAndClear(dimensions, settings.labelGridCellSize); @@ -762,6 +673,8 @@ export default class Sigma extends TypedEventEm this.normalizationFunction = createNormalizationFunction(this.customBBox || this.nodeExtent); const nodesPerPrograms: Record = {}; + const itemIDsIndex: typeof this.itemIDsIndex = {}; + let incrID = 1; let nodes = graph.nodes(); @@ -810,19 +723,18 @@ export default class Sigma extends TypedEventEm if (this.settings.zIndex && nodeZExtent[0] !== nodeZExtent[1]) nodes = zIndexOrdering(nodeZExtent, (node: string): number => this.nodeDataCache[node].zIndex, nodes); - const normalizationRatio = this.normalizationFunction.ratio; for (let i = 0, l = nodes.length; i < l; i++) { const node = nodes[i]; const data = this.nodeDataCache[node]; - this.quadtree.add(node, data.x, 1 - data.y, this.scaleSize(data.size, 1) / normalizationRatio); - if (typeof data.label === "string" && !data.hidden) this.labelGrid.add(node, data.size, this.framedGraphToViewport(data, { matrix: nullCameraMatrix })); const nodeProgram = this.nodePrograms[data.type]; if (!nodeProgram) throw new Error(`Sigma: could not find a suitable program for node type "${data.type}"!`); - nodeProgram.process(nodesPerPrograms[data.type]++, data); + nodeProgram.process(incrID, nodesPerPrograms[data.type]++, data); + itemIDsIndex[incrID] = { type: "node", id: node }; + incrID++; // Save the node in the highlighted set if needed if (data.highlighted && !data.hidden) this.highlightedNodes.add(node); @@ -883,9 +795,13 @@ export default class Sigma extends TypedEventEm sourceData = this.nodeDataCache[extremities[0]], targetData = this.nodeDataCache[extremities[1]]; - this.edgePrograms[data.type].process(edgesPerPrograms[data.type]++, sourceData, targetData, data); + this.edgePrograms[data.type].process(incrID, edgesPerPrograms[data.type]++, sourceData, targetData, data); + itemIDsIndex[incrID] = { type: "edge", id: edge }; + incrID++; } + this.itemIDsIndex = itemIDsIndex; + return this; } @@ -1111,7 +1027,7 @@ export default class Sigma extends TypedEventEm // 3. Process all nodes to render: nodesToRender.forEach((node) => { const data = this.nodeDataCache[node]; - this.nodeHoverPrograms[data.type].process(nodesPerPrograms[data.type]++, data); + this.nodeHoverPrograms[data.type].process(0, nodesPerPrograms[data.type]++, data); }); // 4. Clear hovered nodes layer: this.webGLContexts.hoverNodes.clear(this.webGLContexts.hoverNodes.COLOR_BUFFER_BIT); @@ -1533,6 +1449,7 @@ export default class Sigma extends TypedEventEm this.webGLContexts.nodes.clear(this.webGLContexts.nodes.COLOR_BUFFER_BIT); this.webGLContexts.edges.clear(this.webGLContexts.edges.COLOR_BUFFER_BIT); this.webGLContexts.hoverNodes.clear(this.webGLContexts.hoverNodes.COLOR_BUFFER_BIT); + this.webGLContexts.picking.clear(this.webGLContexts.picking.COLOR_BUFFER_BIT); this.canvasContexts.labels.clearRect(0, 0, this.width, this.height); this.canvasContexts.hovers.clearRect(0, 0, this.width, this.height); this.canvasContexts.edgeLabels.clearRect(0, 0, this.width, this.height); @@ -1801,7 +1718,6 @@ export default class Sigma extends TypedEventEm this.unbindGraphHandlers(); // Releasing cache & state - this.quadtree = new QuadTree(); this.nodeDataCache = {}; this.edgeDataCache = {}; this.nodesWithForcedLabels = []; diff --git a/src/utils/edge-collisions.ts b/src/utils/edge-collisions.ts index 0bcdd95ac..1059dadd8 100644 --- a/src/utils/edge-collisions.ts +++ b/src/utils/edge-collisions.ts @@ -1,17 +1,21 @@ +export function getPixelColor(gl: WebGLRenderingContext, x: number, y: number): [number, number, number, number] { + const pixel = new Uint8Array(4); + gl.readPixels(x, gl.drawingBufferHeight - y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixel); + const [r, g, b, a] = pixel; + return [r, g, b, a]; +} + /** * This helper returns true is the pixel at (x,y) in the given WebGL context is * colored, and false else. */ export function isPixelColored(gl: WebGLRenderingContext, x: number, y: number): boolean { - const pixels = new Uint8Array(4); - gl.readPixels(x, gl.drawingBufferHeight - y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - return pixels[3] > 0; + return getPixelColor(gl, x, y)[3] > 0; } /** - * This helper checks whether or not a point (x, y) collides with an - * edge, connecting a source (xS, yS) to a target (xT, yT) with a thickness in - * pixels. + * This helper checks whether a point (x, y) collides with an edge, connecting a + * source (xS, yS) to a target (xT, yT) with a thickness in pixels. */ export function doEdgeCollideWithPoint( x: number, diff --git a/src/utils/index.ts b/src/utils/index.ts index 76760058c..f8252af36 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -287,15 +287,11 @@ for (const htmlColor in HTML_COLORS) { FLOAT_COLOR_CACHE[HTML_COLORS[htmlColor]] = FLOAT_COLOR_CACHE[htmlColor]; } -export function floatArrayColor(val: string): Float32Array { - val = HTML_COLORS[val] || val; - - // NOTE: this variant is not cached because it is mostly used for uniforms - const { r, g, b, a } = parseColor(val); - - return new Float32Array([r / 255, g / 255, b / 255, a]); +export function rgbaToFloat(r: number, g: number, b: number, a: number, masking?: boolean): number { + INT32[0] = (a << 24) | (b << 16) | (g << 8) | r; + if (masking) INT32[0] = INT32[0] & 0xfeffffff; + return FLOAT32[0]; } - export function floatColor(val: string): number { // If the color is already computed, we yield it if (typeof FLOAT_COLOR_CACHE[val] !== "undefined") return FLOAT_COLOR_CACHE[val]; @@ -303,22 +299,39 @@ export function floatColor(val: string): number { const parsed = parseColor(val); const { r, g, b } = parsed; let { a } = parsed; - a = (a * 255) | 0; - INT32[0] = ((a << 24) | (b << 16) | (g << 8) | r) & 0xfeffffff; - - const color = FLOAT32[0]; + const color = rgbaToFloat(r, g, b, a, true); FLOAT_COLOR_CACHE[val] = color; return color; } +const FLOAT_INDEX_CACHE: { [key: number]: number } = {}; + +export function indexToColor(index: number): number { + // If the index is already computed, we yield it + if (typeof FLOAT_INDEX_CACHE[index] !== "undefined") return FLOAT_INDEX_CACHE[index]; + + const r = (index & 0xff000000) >>> 24; + const g = (index & 0x00ff0000) >>> 16; + const b = (index & 0x0000ff00) >>> 8; + const a = index & 0x000000ff; + + const color = rgbaToFloat(r, g, b, a); + FLOAT_INDEX_CACHE[index] = color; + + return color; +} +export function colorToIndex(r: number, g: number, b: number, a: number): number { + return a + (b << 8) + (g << 16) + (r << 24); +} + /** * In sigma, the graph is normalized into a [0, 1], [0, 1] square, before being given to the various renderers. This - * helps dealing with quadtree in particular. - * But at some point, we need to rescale it so that it takes the best place in the screen, ie. we always want to see two + * helps to deal with quadtree in particular. + * But at some point, we need to rescale it so that it takes the best place in the screen, i.e. we always want to see two * nodes "touching" opposite sides of the graph, with the camera being at its default state. * * This function determines this ratio. @@ -337,7 +350,7 @@ export function getCorrectionRatio( } // Else, we need to fit the graph inside the stage: - // 1. If the graph is "squarer" (ie. with a ratio closer to 1), we need to make the largest sides touch; + // 1. If the graph is "squarer" (i.e. with a ratio closer to 1), we need to make the largest sides touch; // 2. If the stage is "squarer", we need to make the smallest sides touch. return Math.min(Math.max(graphRatio, 1 / graphRatio), Math.max(1 / viewportRatio, viewportRatio)); } @@ -400,15 +413,15 @@ export function matrixFromCamera( * * [jacomyal] * To be fully honest, I can't really explain happens here... I notice that the - * following ratio works (ie. it correctly compensates the matrix impact on all + * following ratio works (i.e. it correctly compensates the matrix impact on all * camera states I could try): * > `R = size(V) / size(M * V) / W` * as long as `M * V` is in the direction of W (ie. parallel to (Ox)). It works * as well with H and a vector that transforms into something parallel to (Oy). * * Also, note that we use `angle` and not `-angle` (that would seem logical, - * since we want to anticipate the rotation), because of the fact that in WebGL, - * the image is vertically swapped. + * since we want to anticipate the rotation), because the image is vertically + * swapped in WebGL. */ export function getMatrixImpact( matrix: Float32Array, diff --git a/test/unit/quadtree.ts b/test/unit/quadtree.ts deleted file mode 100644 index 84f9e0e47..000000000 --- a/test/unit/quadtree.ts +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Sigma.js QuadTree Unit Tests - * ============================= - * - * Testing the quadtree class. - */ -import assert from "assert"; -import QuadTree, { getCircumscribedAlignedRectangle, isRectangleAligned, Rectangle } from "../../src/core/quadtree"; - -describe("QuadTree geometry utils", () => { - describe("QuadTree#getCircumscribedAlignedRectangle", () => { - it("should return the given rectangle for 'straight' rectangles", () => { - const rect: Rectangle = { - x1: 0, - y1: 0, - x2: 1, - y2: 0, - height: 1, - }; - - assert.deepStrictEqual(rect, getCircumscribedAlignedRectangle(rect)); - }); - - it("should return the good circumscribed rectangle for 'tilted' rectangles", () => { - const rect: Rectangle = { - x1: 0, - y1: 0, - x2: 1, - y2: -1, - height: Math.SQRT2, - }; - - assert.deepStrictEqual(getCircumscribedAlignedRectangle(rect), { - x1: 0, - y1: -1, - x2: 2, - y2: -1, - height: 2, - }); - }); - }); - - describe("QuadTree#isRectangleAligned", () => { - it("should work with aligned rectangles", () => { - assert.ok( - isRectangleAligned({ - x1: 0, - y1: 0, - x2: 1, - y2: 0, - height: 1, - }), - ); - assert.ok( - isRectangleAligned({ - x1: 0, - y1: 0, - x2: 0, - y2: -2, - height: 1, - }), - ); - }); - - it("should work with misaligned rectangles", () => { - assert.ok( - !isRectangleAligned({ - x1: 0, - y1: 0, - x2: 1, - y2: -1, - height: Math.SQRT2, - }), - ); - assert.ok( - !isRectangleAligned({ - x1: 0, - y1: 0, - x2: 1, - y2: 1, - height: Math.SQRT2, - }), - ); - }); - }); -}); - -describe("QuadTree", function () { - const nodes = [ - { - key: "a", - x: 394, - y: 10, - size: 1, - }, - { - key: "b", - x: 12, - y: 10, - size: 3, - }, - ]; - - const tree = new QuadTree({ boundaries: { x: 0, y: 0, width: 500, height: 500 } }); - - nodes.forEach((node) => tree.add(node.key, node.x, node.y, node.size)); - - // console.log(tree.point(10, 14)); -});