@jonobr1/force-directed-graph
Version:
GPU supercharged attraction-graph visualizations for the web built on top of Three.js
1,709 lines (1,538 loc) • 105 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.js
var index_exports = {};
__export(index_exports, {
ForceDirectedGraph: () => ForceDirectedGraph
});
module.exports = __toCommonJS(index_exports);
var import_three6 = require("three");
var import_GPUComputationRenderer = require("three/examples/jsm/misc/GPUComputationRenderer.js");
// src/math.js
var pot = [2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096];
function getPotSize(number) {
const side = Math.floor(Math.sqrt(number)) + 1;
for (let i = 0; i < pot.length; i++) {
if (pot[i] >= side) {
return pot[i];
}
}
console.error(
"ForceDirectedGraph: Texture size is too big.",
"Consider reducing the size of your data."
);
}
function clamp(x, min, max) {
return Math.min(Math.max(x, min), max);
}
var maxFrames = 1e3;
function each(list, func, step, max) {
if (typeof step !== "number") {
step = 1;
}
if (typeof max !== "number") {
max = maxFrames;
}
return new Promise((resolve) => {
exec(0);
function exec(start) {
const limit = Math.min(start + maxFrames, list.length);
let i = start;
while (i < limit) {
func(list[i], i);
i += step;
}
if (limit < list.length) {
requestAnimationFrame(() => exec(i));
} else {
resolve();
}
}
});
}
function rgbToIndex({ r, g, b }) {
return r + g * 255 + b * Math.pow(255, 2);
}
// src/shaders/positions.js
var positions = `
uniform float is2D;
uniform float timeStep;
void main() {
vec2 uv = gl_FragCoord.xy / resolution.xy;
vec4 texel = texture2D( texturePositions, uv );
vec3 position = texel.xyz;
vec3 velocity = texture2D( textureVelocities, uv ).xyz;
float isStatic = texel.w;
vec3 result = position + velocity * timeStep * ( 1.0 - isStatic );
gl_FragColor = vec4( result.xyz, isStatic );
}
`;
// src/shaders/partials.js
var circle = `
float circle( vec2 uv, vec2 pos, float rad, float isSmooth ) {
float limit = 0.02;
float limit2 = limit * 2.0;
float d = length( pos - uv ) - ( rad - limit );
float t = clamp( d, 0.0, 1.0 );
float viewRange = smoothstep( 0.0, frustumSize * 0.001, abs( vDistance ) );
float taper = limit2 * viewRange + limit;
taper = mix( taper, limit2, sizeAttenuation );
float a = step( 0.5, 1.0 - t );
float aa = smoothstep( 0.5 - taper, 0.5 + taper, 1.0 - t );;
return mix( a, aa, isSmooth );
}
`;
var getPosition = `
vec3 getPosition( vec2 uv ) {
return texture2D( texturePositions, uv ).xyz;
}
`;
var getVelocity = `
vec3 getVelocity( vec2 uv ) {
return texture2D( textureVelocities, uv ).xyz;
}
`;
var getIndex = `
int getIndex( vec2 uv ) {
int s = int( size );
int col = int( uv.x * size );
int row = int( uv.y * size );
return col + row * s;
}
`;
var getUVFromIndex = `
vec2 getUVFromIndex( float i ) {
float uvx = mod( i, size ) / size;
float uvy = floor( i / size ) / size;
return vec2( uvx, uvy );
}
`;
var random = `
float random( vec2 seed ) {
return fract( sin( dot( seed.xy, vec2( 12.9898, 78.233 ) ) ) * 43758.5453 );
}
`;
var jiggle = `
float jiggle( float index ) {
return ( random( vec2( index, time ) ) - 0.5 ) * 0.000001;
}
`;
var link = `
vec3 link( int id1, vec2 uv2, float rangeStart, float rangeEnd ) {
vec3 result = vec3( 0.0 );
vec4 edge = texture2D( textureLinks, uv2 );
vec2 source = edge.xy;
vec2 target = edge.zw;
int si = getIndex( source );
float siF = float( si );
vec3 sv = getVelocity( source );
vec3 sp = getPosition( source );
int ti = getIndex( target );
float tiF = float( ti );
vec3 tv = getVelocity( target );
vec3 tp = getPosition( target );
vec3 diff = tp + tv - ( sp + sv );
diff.z *= 1.0 - is2D;
vec3 mag = abs( diff );
float seed = float( si + ti );
float bias = 0.5;
float dist = length( diff );
dist = stiffness * ( dist - springLength ) / dist;
diff *= dist;
if ( id1 == ti ) {
result -= diff * bias;
} else if ( id1 == si ) {
result += diff * bias;
}
result.z *= 1.0 - is2D;
float siInRange = step( rangeStart, siF ) * ( 1.0 - step( rangeEnd, siF ) );
float tiInRange = step( rangeStart, tiF ) * ( 1.0 - step( rangeEnd, tiF ) );
return result * siInRange * tiInRange;
}
`;
var charge = `
vec3 charge( float i, int id1, vec3 p1, vec3 v1, int id2, vec3 v2, vec3 p2 ) {
vec3 result = vec3( 0.0 );
vec3 diff = ( p2 + v2 ) - ( p1 + v1 );
diff.z *= 1.0 - is2D;
float dist = length( diff );
float mag = repulsion / dist;
vec3 dir = normalize( diff );
if ( id1 != id2 ) {
result += dir * mag;
}
result.z *= 1.0 - is2D;
return result;
}
`;
var center = `
vec3 center( vec3 p1 ) {
return - p1 * gravity * 0.1;
}
`;
var anchor = `
vec3 anchor( vec3 p1, vec3 target ) {
return ( target - p1 ) * gravity * 0.1;
}
`;
// src/shaders/velocities.js
var types = ["simplex", "nested"];
var simplex = `
uniform float alpha;
uniform float is2D;
uniform float size;
uniform float time;
uniform float nodeRadius;
uniform float nodeAmount;
uniform float edgeAmount;
uniform float maxSpeed;
uniform float timeStep;
uniform float damping;
uniform float repulsion;
uniform float springLength;
uniform float stiffness;
uniform float gravity;
uniform float pinStrength;
uniform float uBeginning;
uniform float uEnding;
uniform sampler2D textureLinks;
uniform sampler2D textureLinkRanges;
uniform sampler2D textureTargetPositions;
${getPosition}
${getVelocity}
${getIndex}
${getUVFromIndex}
${random}
${jiggle}
${link}
${charge}
${center}
${anchor}
void main() {
vec2 uv = gl_FragCoord.xy / resolution.xy;
int id1 = getIndex( uv );
vec3 p1 = getPosition( uv );
vec3 v1 = getVelocity( uv );
float rangeStart = uBeginning * nodeAmount;
float rangeEnd = uEnding * nodeAmount;
vec3 a = vec3( 0.0 ),
b = vec3( 0.0 ),
c = vec3( 0.0 );
vec4 linkRange = texture2D( textureLinkRanges, uv );
float linkStart = linkRange.x;
float linkCount = linkRange.y;
for ( float i = 0.0; i < edgeAmount; i += 1.0 ) {
if ( i >= linkCount ) {
break;
}
vec2 linkUV = getUVFromIndex( linkStart + i );
b += link( id1, linkUV, rangeStart, rangeEnd );
}
for ( float i = 0.0; i < nodeAmount; i += 1.0 ) {
vec2 uv2 = getUVFromIndex( i );
int id2 = getIndex( uv2 );
vec3 v2 = getVelocity( uv2 );
vec3 p2 = getPosition( uv2 );
float id2InRange = step( rangeStart, i ) * ( 1.0 - step( rangeEnd, i ) );
c += charge( i, id1, p1, v1, id2, p2, v2 ) * id2InRange;
}
float id1InRange = step( rangeStart, float( id1 ) ) * ( 1.0 - step( rangeEnd, float( id1 ) ) );
b *= id1InRange;
c *= id1InRange;
// 4.
vec4 targetTexel = texture2D( textureTargetPositions, uv );
vec3 d = mix( center( p1 ), anchor( p1, targetTexel.xyz ), pinStrength * targetTexel.w );
vec3 acceleration = a + b + c + d * id1InRange;
// Calculate Velocity
vec3 velocity = ( v1 + ( acceleration * timeStep ) ) * damping * alpha;
velocity = clamp( velocity, - maxSpeed, maxSpeed );
velocity.z *= 1.0 - is2D;
gl_FragColor = vec4( velocity, 0.0 );
}
`;
var nested = `
uniform float alpha;
uniform float is2D;
uniform float size;
uniform float time;
uniform float nodeRadius;
uniform float nodeAmount;
uniform float edgeAmount;
uniform float maxSpeed;
uniform float timeStep;
uniform float damping;
uniform float repulsion;
uniform float springLength;
uniform float stiffness;
uniform float gravity;
uniform float pinStrength;
uniform float uBeginning;
uniform float uEnding;
uniform sampler2D textureLinks;
uniform sampler2D textureLinksLookUp;
uniform sampler2D textureTargetPositions;
${getPosition}
${getVelocity}
${getIndex}
${getUVFromIndex}
${random}
${jiggle}
${link}
${charge}
${center}
${anchor}
void main() {
vec2 uv = gl_FragCoord.xy / resolution.xy;
int id1 = getIndex( uv );
vec3 p1 = getPosition( uv );
vec3 v1 = getVelocity( uv );
float rangeStart = uBeginning * nodeAmount;
float rangeEnd = uEnding * nodeAmount;
vec3 a = vec3( 0.0 ),
b = vec3( 0.0 ),
c = vec3( 0.0 );
/*
for ( float i = 0.0; i < linkAmount; i += 1.0 ) {
// TODO: get all edges and link them
b += link( id1, uv2, rangeStart, rangeEnd );
}
*/
for ( float i = 0.0; i < nodeAmount; i += 1.0 ) {
float uvx = mod( i, size ) / size;
float uvy = floor( i / size ) / size;
vec2 uv2 = vec2( uvx, uvy );
int id2 = getIndex( uv2 );
vec3 v2 = getVelocity( uv2 );
vec3 p2 = getPosition( uv2 );
float id2InRange = step( rangeStart, i ) * ( 1.0 - step( rangeEnd, i ) );
if ( i < nodeAmount) {
c += charge( i, id1, p1, v1, id2, p2, v2 ) * id2InRange;
}
}
float id1InRange = step( rangeStart, float( id1 ) ) * ( 1.0 - step( rangeEnd, float( id1 ) ) );
b *= id1InRange;
c *= id1InRange;
// 4.
vec4 targetTexel = texture2D( textureTargetPositions, uv );
vec3 d = mix( center( p1 ), anchor( p1, targetTexel.xyz ), pinStrength * targetTexel.w );
vec3 acceleration = a + b + c + d * id1InRange;
// Calculate Velocity
vec3 velocity = ( v1 + ( acceleration * timeStep ) ) * damping * alpha;
velocity = clamp( velocity, - maxSpeed, maxSpeed );
velocity.z *= 1.0 - is2D;
gl_FragColor = vec4( velocity, 0.0 );
}
`;
// src/shaders/simulation.js
var simulation_default = {
positions,
velocities: simplex,
simplex,
nested,
types
};
// src/points.js
var import_three2 = require("three");
// src/shaders/points.js
var points = {
vertexShader: `
#include <fog_pars_vertex>
uniform float sizeAttenuation;
uniform float frustumSize;
uniform float is2D;
uniform float nodeRadius;
uniform float nodeScale;
uniform float uBeginning;
uniform float uEnding;
uniform float uNodeAmount;
uniform sampler2D texturePositions;
uniform sampler2D textureTargetPositions;
varying vec3 vColor;
varying float vImageKey;
varying float vDistance;
varying float vViewZ;
varying vec3 vTargetPosition;
varying float vHasTarget;
attribute float imageKey;
attribute float pointSize;
void main() {
float nodeIndex = position.z - 1.0;
float rangeStart = uBeginning * uNodeAmount;
float rangeEnd = uEnding * uNodeAmount;
float inRange = step( rangeStart, nodeIndex ) * ( 1.0 - step( rangeEnd, nodeIndex ) );
vec4 texel = texture2D( texturePositions, position.xy );
vec3 vPosition = texel.xyz;
vPosition.z *= 1.0 - is2D;
vec4 targetTexel = texture2D( textureTargetPositions, position.xy );
vTargetPosition = targetTexel.xyz;
vHasTarget = targetTexel.w;
vec4 mvPosition = modelViewMatrix * vec4( vPosition, 1.0 );
gl_PointSize = nodeRadius * pointSize * nodeScale;
gl_PointSize *= mix( 1.0, frustumSize / - mvPosition.z, sizeAttenuation );
gl_PointSize *= inRange;
vDistance = 1.0 / - mvPosition.z;
vViewZ = mvPosition.z;
vColor = color;
vImageKey = imageKey;
gl_Position = projectionMatrix * mvPosition;
#include <fog_vertex>
}
`,
fragmentShader: `
#include <fog_pars_fragment>
uniform float sizeAttenuation;
uniform float frustumSize;
uniform vec3 uColor;
uniform float opacity;
uniform float imageDimensions;
uniform sampler2D textureAtlas;
uniform float inheritColors;
varying vec3 vColor;
varying float vImageKey;
varying float vDistance;
varying float vViewZ;
void main() {
// Calculate distance from center for circular shape and depth
vec2 cxy = 2.0 * gl_PointCoord - 1.0;
float r = length(cxy);
// Antialiased circle using fwidth for automatic edge smoothing
float delta = fwidth(r);
float t = 1.0 - smoothstep(1.0 - delta, 1.0, r);
// Calculate custom depth to fix z-fighting with transparent points
// For fragments inside the circle, offset depth proportionally
#if defined(GL_EXT_frag_depth)
if (r <= 1.0) {
// Keep the center of the node slightly closer so coincident links
// do not leak through overlapping nodes.
float depthOffset = (1.0 - r) * 0.0001;
gl_FragDepthEXT = gl_FragCoord.z - depthOffset;
} else {
gl_FragDepthEXT = gl_FragCoord.z;
}
#elif __VERSION__ >= 300
if (r <= 1.0) {
// Keep the center of the node slightly closer so coincident links
// do not leak through overlapping nodes.
float depthOffset = (1.0 - r) * 0.0001;
gl_FragDepth = gl_FragCoord.z - depthOffset;
} else {
gl_FragDepth = gl_FragCoord.z;
}
#endif
// Calculate texture atlas coordinates for image sprites
float col = mod( vImageKey, imageDimensions );
float row = floor( vImageKey / imageDimensions );
vec2 uv = vec2( 0.0 );
uv.x = mix( 0.0, 1.0 / imageDimensions, gl_PointCoord.x );
uv.y = mix( 0.0, 1.0 / imageDimensions, gl_PointCoord.y );
uv = vec2( gl_PointCoord ) / imageDimensions;
uv.x += col / imageDimensions;
uv.y += row / imageDimensions;
vec4 texel = texture2D( textureAtlas, uv );
float useImage = step( 0.0, vImageKey );
t = mix( t, texel.a, useImage );
vec3 layer = mix( vec3( 1.0 ), texel.rgb, useImage );
float alpha = opacity * t;
if ( alpha <= 0.0 ) {
discard;
}
gl_FragColor = vec4( layer * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor, alpha );
#include <fog_fragment>
}
`
};
var points_default = points;
// src/texture-atlas.js
var import_three = require("three");
var anchor2;
var TextureAtlas = class _TextureAtlas extends import_three.Texture {
map = [];
dimensions = 1;
isTextureAtlas = true;
constructor() {
if (!anchor2) {
anchor2 = document.createElement("a");
}
super(document.createElement("canvas"));
this.flipY = false;
}
static Resolution = 1024;
static getAbsoluteURL(path) {
anchor2.href = path;
return anchor2.href;
}
add(src) {
const scope = this;
let img, index;
if (typeof src === "string") {
index = this.indexOf(src);
if (index >= 0) {
img = this.map[index];
if (img.complete) {
onLoad();
} else {
img.addEventListener("load", onLoad, false);
}
} else {
img = document.createElement("img");
img.addEventListener("load", onLoad, false);
img.src = src;
index = this.map.length;
this.map.push(img);
}
} else if (typeof src === "object" && "src" in src) {
img = src;
src = img.src;
index = this.indexOf(src);
if (index >= 0) {
img = this.map[index];
} else {
index = this.map.length;
this.map.push(img);
}
if (img.complete) {
onLoad();
} else {
img.addEventListener("load", onLoad, false);
}
}
this.dimensions = Math.ceil(Math.sqrt(this.map.length));
return index;
function onLoad() {
img.removeEventListener("load", onLoad, false);
scope.update();
}
}
update() {
const { image } = this;
const ctx = image.getContext("2d");
image.width = _TextureAtlas.Resolution;
image.height = _TextureAtlas.Resolution;
const dims = this.dimensions = Math.ceil(Math.sqrt(this.map.length));
const width = image.width / dims;
const height = image.height / dims;
ctx.clearRect(0, 0, image.width, image.height);
for (let i = 0; i < this.map.length; i++) {
const col = i % dims;
const row = Math.floor(i / dims);
const img = this.map[i];
const x = col / dims * image.width;
const y = row / dims * image.height;
ctx.drawImage(img, x, y, width, height);
}
this.needsUpdate = true;
}
indexOf(src) {
const uri = _TextureAtlas.getAbsoluteURL(src);
for (let i = 0; i < this.map.length; i++) {
const img = this.map[i];
if (uri === img.src) {
return i;
}
}
return -1;
}
};
// src/points.js
var color = new import_three2.Color();
var Points = class extends import_three2.Points {
constructor({ atlas, geometry }, uniforms) {
const material = new import_three2.ShaderMaterial({
uniforms: { ...import_three2.UniformsLib["fog"], ...{
is2D: uniforms.is2D,
sizeAttenuation: uniforms.sizeAttenuation,
frustumSize: uniforms.frustumSize,
nodeRadius: uniforms.nodeRadius,
nodeScale: uniforms.nodeScale,
imageDimensions: { value: atlas.dimensions },
texturePositions: { value: null },
textureTargetPositions: { value: null },
textureAtlas: { value: atlas },
size: uniforms.size,
opacity: uniforms.opacity,
uColor: uniforms.pointColor,
inheritColors: uniforms.pointsInheritColor,
uBeginning: uniforms.uBeginning,
uEnding: uniforms.uEnding,
uNodeAmount: uniforms.uNodeAmount
} },
vertexShader: points_default.vertexShader,
fragmentShader: points_default.fragmentShader,
transparent: true,
vertexColors: true,
fog: true
});
super(geometry, material);
this.frustumCulled = false;
}
static parse(size2, data) {
const atlas = new TextureAtlas();
const vertices2 = [];
const colors = [];
const imageKeys = [];
const sizes = [];
return each(data.nodes, (_, i) => {
const node = data.nodes[i];
const x = i % size2 / size2;
const y = Math.floor(i / size2) / size2;
const z = i + 1;
vertices2.push(x, y, z);
if (node.color) {
color.set(node.color);
colors.push(color.r, color.g, color.b);
} else {
colors.push(1, 1, 1);
}
if (node.image) {
imageKeys.push(atlas.add(node.image));
} else {
imageKeys.push(-1);
}
sizes.push(node.size != null ? node.size : 1);
}).then(() => {
const geometry = new import_three2.BufferGeometry();
geometry.setAttribute(
"position",
new import_three2.Float32BufferAttribute(vertices2, 3)
);
geometry.setAttribute(
"color",
new import_three2.Float32BufferAttribute(colors, 3)
);
geometry.setAttribute(
"imageKey",
new import_three2.Float32BufferAttribute(imageKeys, 1)
);
geometry.setAttribute(
"pointSize",
new import_three2.Float32BufferAttribute(sizes, 1)
);
return { atlas, geometry };
});
}
};
// src/links.js
var import_three3 = require("three");
// src/shaders/links.js
var links = {
vertexShader: `
#include <fog_pars_vertex>
uniform float frustumSize;
uniform float is2D;
uniform float linewidth;
uniform float pixelRatio;
uniform float sizeAttenuation;
uniform float uBeginning;
uniform float uEnding;
uniform float uNodeAmount;
uniform vec2 resolution;
uniform sampler2D texturePositions;
attribute vec3 source;
attribute vec3 target;
attribute vec3 sourceColor;
attribute vec3 targetColor;
varying vec2 vSource;
varying vec2 vTarget;
varying vec3 vSourceColor;
varying vec3 vTargetColor;
varying float vHalfWidth;
varying float inRange;
void main() {
float sourceIndex = source.z - 1.0;
float targetIndex = target.z - 1.0;
float rangeStart = uBeginning * uNodeAmount;
float rangeEnd = uEnding * uNodeAmount;
float sourceInRange = step( rangeStart, sourceIndex ) * ( 1.0 - step( rangeEnd, sourceIndex ) );
float targetInRange = step( rangeStart, targetIndex ) * ( 1.0 - step( rangeEnd, targetIndex ) );
vec3 sourcePosition = texture2D( texturePositions, source.xy ).xyz;
vec3 targetPosition = texture2D( texturePositions, target.xy ).xyz;
sourcePosition.z *= 1.0 - is2D;
targetPosition.z *= 1.0 - is2D;
vec4 sourceModelView = modelViewMatrix * vec4( sourcePosition, 1.0 );
vec4 targetModelView = modelViewMatrix * vec4( targetPosition, 1.0 );
vec4 sourceClip = projectionMatrix * sourceModelView;
vec4 targetClip = projectionMatrix * targetModelView;
vec2 safeResolution = max( resolution, vec2( 1.0 ) );
vec2 sourceNdc = sourceClip.xy / sourceClip.w;
vec2 targetNdc = targetClip.xy / targetClip.w;
vec2 sourceScreen = ( sourceNdc * 0.5 + 0.5 ) * safeResolution;
vec2 targetScreen = ( targetNdc * 0.5 + 0.5 ) * safeResolution;
vec2 delta = targetScreen - sourceScreen;
float segmentLength = length( delta );
vec2 tangent = segmentLength > 0.0 ? delta / segmentLength : vec2( 1.0, 0.0 );
vec2 normal = vec2( - tangent.y, tangent.x );
float centerViewZ = 0.5 * ( sourceModelView.z + targetModelView.z );
float widthScale = mix(
1.0,
frustumSize / max( -centerViewZ, 0.0001 ),
sizeAttenuation
);
float halfWidth = max( 0.5 * linewidth * pixelRatio * widthScale, 0.5 );
float expansion = halfWidth + 1.0;
float edgeT = position.x * 0.5 + 0.5;
vec2 base = mix( sourceScreen, targetScreen, edgeT );
vec2 screen = base + tangent * position.x * expansion + normal * position.y * expansion;
vec2 ndc = ( screen / safeResolution ) * 2.0 - 1.0;
float clipW = mix( sourceClip.w, targetClip.w, edgeT );
float clipZ = mix( sourceClip.z, targetClip.z, edgeT );
gl_Position = vec4( ndc * clipW, clipZ, clipW );
vec4 mvPosition = mix( sourceModelView, targetModelView, edgeT );
vSource = sourceScreen;
vTarget = targetScreen;
vSourceColor = sourceColor;
vTargetColor = targetColor;
vHalfWidth = halfWidth;
inRange = sourceInRange * targetInRange;
#include <fog_vertex>
}
`,
fragmentShader: `
#include <fog_pars_fragment>
uniform float inheritColors;
uniform float linecap;
uniform vec3 uColor;
uniform float opacity;
varying vec2 vSource;
varying vec2 vTarget;
varying vec3 vSourceColor;
varying vec3 vTargetColor;
varying float vHalfWidth;
varying float inRange;
float getSegmentT( vec2 point, vec2 start, vec2 end ) {
vec2 segment = end - start;
float lengthSquared = dot( segment, segment );
if ( lengthSquared <= 0.0 ) {
return 0.0;
}
return clamp( dot( point - start, segment ) / lengthSquared, 0.0, 1.0 );
}
float getCapsuleDistance( vec2 point, vec2 start, vec2 end, float radius ) {
float t = getSegmentT( point, start, end );
vec2 closest = mix( start, end, t );
return length( point - closest ) - radius;
}
float getRectDistance( vec2 point, vec2 start, vec2 end, vec2 extent ) {
vec2 segment = end - start;
float segmentLength = length( segment );
if ( segmentLength <= 0.0 ) {
return length( point - start ) - extent.y;
}
vec2 tangent = segment / segmentLength;
vec2 normal = vec2( - tangent.y, tangent.x );
vec2 local = vec2(
dot( point - start, tangent ) - 0.5 * segmentLength,
dot( point - start, normal )
);
vec2 delta = abs( local ) - extent;
return length( max( delta, 0.0 ) ) + min( max( delta.x, delta.y ), 0.0 );
}
float getLinkDistance( vec2 point, vec2 start, vec2 end, float radius ) {
vec2 segment = end - start;
float segmentLength = length( segment );
if ( segmentLength <= 0.0 ) {
return length( point - start ) - radius;
}
if ( linecap < 0.5 ) {
return getCapsuleDistance( point, start, end, radius );
}
if ( linecap < 1.5 ) {
return getRectDistance(
point,
start,
end,
vec2( 0.5 * segmentLength, radius )
);
}
return getRectDistance(
point,
start,
end,
vec2( 0.5 * segmentLength + radius, radius )
);
}
void main() {
if ( inRange < 0.5 ) {
discard;
}
float segmentT = getSegmentT( gl_FragCoord.xy, vSource, vTarget );
float distanceToCapsule = getLinkDistance(
gl_FragCoord.xy,
vSource,
vTarget,
vHalfWidth
);
float alpha = 1.0 - smoothstep( 0.0, 1.0, distanceToCapsule );
if ( alpha <= 0.0 ) {
discard;
}
vec3 gradient = mix( vSourceColor, vTargetColor, segmentT );
gl_FragColor = vec4(
mix( vec3( 1.0 ), gradient, inheritColors ) * uColor,
opacity * alpha
);
#include <fog_fragment>
}
`
};
var links_default = links;
// src/links.js
var vertices = new Float32Array([-1, -1, 0, 1, -1, 0, -1, 1, 0, 1, 1, 0]);
var indices = [0, 1, 2, 2, 1, 3];
var Links = class extends import_three3.Mesh {
constructor(geometry, uniforms) {
const material = new import_three3.ShaderMaterial({
uniforms: {
...import_three3.UniformsLib["fog"],
...{
frustumSize: uniforms.frustumSize,
is2D: uniforms.is2D,
inheritColors: uniforms.linksInheritColor,
linecap: uniforms.linecap,
linewidth: uniforms.linewidth,
opacity: uniforms.opacity,
pixelRatio: uniforms.pixelRatio,
resolution: uniforms.resolution,
sizeAttenuation: uniforms.sizeAttenuation,
texturePositions: { value: null },
uColor: uniforms.linkColor,
uBeginning: uniforms.uBeginning,
uEnding: uniforms.uEnding,
uNodeAmount: uniforms.uNodeAmount
}
},
vertexShader: links_default.vertexShader,
fragmentShader: links_default.fragmentShader,
transparent: true,
fog: true,
side: import_three3.DoubleSide
});
super(geometry, material);
this.frustumCulled = false;
}
static parse(points2, data) {
const geometry = new import_three3.InstancedBufferGeometry();
const sources = [];
const targets = [];
const sourceColors = [];
const targetColors = [];
const v = points2.geometry.attributes.position.array;
const c = points2.geometry.attributes.color.array;
geometry.setAttribute("position", new import_three3.BufferAttribute(vertices, 3));
geometry.setIndex(indices);
return each(data.links, (_, i) => {
const link2 = data.links[i];
const sourceIndex = 3 * link2.sourceIndex;
const targetIndex = 3 * link2.targetIndex;
sources.push(v[sourceIndex + 0], v[sourceIndex + 1], v[sourceIndex + 2]);
targets.push(v[targetIndex + 0], v[targetIndex + 1], v[targetIndex + 2]);
sourceColors.push(
c[sourceIndex + 0],
c[sourceIndex + 1],
c[sourceIndex + 2]
);
targetColors.push(
c[targetIndex + 0],
c[targetIndex + 1],
c[targetIndex + 2]
);
}).then(() => {
geometry.setAttribute(
"source",
new import_three3.InstancedBufferAttribute(new Float32Array(sources), 3)
);
geometry.setAttribute(
"target",
new import_three3.InstancedBufferAttribute(new Float32Array(targets), 3)
);
geometry.setAttribute(
"sourceColor",
new import_three3.InstancedBufferAttribute(new Float32Array(sourceColors), 3)
);
geometry.setAttribute(
"targetColor",
new import_three3.InstancedBufferAttribute(new Float32Array(targetColors), 3)
);
geometry.instanceCount = data.links.length;
return geometry;
});
}
};
// src/labels.js
var import_three4 = require("three");
// src/shaders/labels.js
var labels = {
vertexShader: `
#include <fog_pars_vertex>
uniform sampler2D texturePositions;
uniform float frustumSize;
uniform float is2D;
uniform float sizeAttenuation;
uniform vec2 resolution;
uniform float uBeginning;
uniform float uEnding;
uniform float uNodeAmount;
uniform float obscurity;
uniform float nodeRadius;
uniform float nodeScale;
uniform float uLabelCount;
uniform float labelAlignment;
uniform float labelBaseline;
uniform float labelFontSize;
uniform float labelNear;
uniform vec2 labelOffset;
attribute vec3 source; // .xy = UV into texturePositions, .z = nodeIndex + 1
attribute vec4 labelUV; // .xy = atlas UV offset, .zw = atlas UV extent
attribute float aspectRatio; // label quad width / height
attribute float pointSize; // per-node point size scalar
attribute float selectionRank;
varying vec2 vLabelUV;
varying vec3 vColor;
varying float vInRange;
void main() {
float nodeIndex = source.z - 1.0;
float rangeStart = uBeginning * uNodeAmount;
float rangeEnd = uEnding * uNodeAmount;
float inRange = step( rangeStart, nodeIndex ) * ( 1.0 - step( rangeEnd, nodeIndex ) );
float visibleCount = floor( ( 1.0 - clamp( obscurity, 0.0, 1.0 ) ) * uLabelCount + 0.5 );
float rankVisible = step( selectionRank + 0.5, visibleCount );
inRange *= rankVisible;
vec3 nodePos = texture2D( texturePositions, source.xy ).xyz;
nodePos.z *= 1.0 - is2D;
vec4 mvCenter = modelViewMatrix * vec4( nodePos, 1.0 );
float viewDistance = -mvCenter.z;
float beyondNear = 1.0 - step( viewDistance, max( labelNear, 0.0 ) );
inRange *= beyondNear;
// Billboard: extract camera right and up from the corresponding view matrix rows
vec3 right = normalize( vec3( viewMatrix[0][0], viewMatrix[1][0], viewMatrix[2][0] ) );
vec3 up = normalize( vec3( viewMatrix[0][1], viewMatrix[1][1], viewMatrix[2][1] ) );
// Match point-sprite sizing by converting the intended screen-space
// label height into world units for the active projection.
float sizeScale = mix( 1.0, frustumSize / max( viewDistance, 0.001 ), sizeAttenuation );
float labelPixelH = 0.1 * nodeRadius * pointSize * nodeScale * sizeScale * max( labelFontSize, 0.001 );
float projectionScaleY = max( abs( projectionMatrix[1][1] ), 0.0001 );
float isPerspectiveCamera = step( 0.5, abs( projectionMatrix[2][3] ) );
float depthScale = mix( 1.0, viewDistance, isPerspectiveCamera );
float worldUnitsPerPixel = ( 2.0 * depthScale ) / max( projectionScaleY * max( resolution.y, 1.0 ), 0.001 );
float labelH = labelPixelH * worldUnitsPerPixel;
float labelW = labelH * aspectRatio;
vec2 offset = labelOffset * labelH;
// Shift the label relative to the node according to baseline/alignment.
vec3 worldPos = nodePos
+ right * ( labelW * 0.5 * labelAlignment + offset.x )
+ up * ( labelH * labelBaseline + offset.y )
+ right * position.x * labelW * 0.5
+ up * position.y * labelH * 0.5;
// Map quad UV [0,1] to the atlas region for this label
vLabelUV = labelUV.xy + uv * labelUV.zw;
vColor = color;
vInRange = inRange;
vec4 mvPosition = modelViewMatrix * vec4( worldPos, 1.0 );
gl_Position = projectionMatrix * mvPosition;
#include <fog_vertex>
}
`,
fragmentShader: `
#include <fog_pars_fragment>
uniform sampler2D textureAtlas;
uniform float inheritColors;
uniform float opacity;
uniform vec3 uColor;
varying vec2 vLabelUV;
varying vec3 vColor;
varying float vInRange;
void main() {
if ( vInRange <= 0.0 ) {
discard;
}
vec4 texel = texture2D( textureAtlas, vLabelUV );
float alpha = opacity * texel.a;
if ( alpha <= 0.0 ) {
discard;
}
gl_FragColor = vec4(
texel.rgb * mix( vec3( 1.0 ), vColor, inheritColors ) * uColor,
alpha
);
#include <fog_fragment>
#ifdef USE_FOG
if ( fogFactor > 0.5 ) {
discard;
}
#endif
}
`
};
var labels_default = labels;
// src/labels.js
var MODEL_VIEW_MATRIX = new import_three4.Matrix4();
var CAMERA_RIGHT = new import_three4.Vector3();
var CAMERA_UP = new import_three4.Vector3();
var LOCAL_NODE = new import_three4.Vector3();
var WORLD_CENTER = new import_three4.Vector3();
var WORLD_CORNER = new import_three4.Vector3();
var PROJECTED_CORNER = new import_three4.Vector3();
var MV_CENTER = new import_three4.Vector4();
var BASE_ATLAS_FONT_SIZE = 120;
var BASE_ATLAS_PADDING = 4;
var ATLAS_RASTER_SCALE = 2;
var DEFAULT_MAX_LABEL_CANVAS_SIZE = 4096;
var DEFAULT_FONT_FAMILY = "Arial, sans-serif";
var LABEL_GRAPH_DISTANCE_HOPS = 6;
var LABEL_NODE_COLOR = new import_three4.Color();
var LabelAlignmentMap = {
center: 0,
left: 1,
right: -1
};
var LabelBaselineMap = {
top: 1,
middle: 0,
bottom: -1
};
function getLabelAlignmentName(value) {
if (value > 0.5) {
return "left";
}
if (value < -0.5) {
return "right";
}
return "center";
}
function getLabelBaselineName(value) {
if (value > 0.5) {
return "top";
}
if (value < -0.5) {
return "bottom";
}
return "middle";
}
function sanitizeLabelFontSize(fontSize) {
if (!Number.isFinite(fontSize)) {
return 1;
}
return Math.max(0.01, fontSize);
}
function sanitizeLabelNearDistance(nearDistance) {
if (!Number.isFinite(nearDistance)) {
return 0;
}
return Math.max(0, nearDistance);
}
function sanitizePositiveInteger(value, fallback) {
if (!Number.isFinite(value) || value <= 0) {
return fallback;
}
return Math.max(1, Math.floor(value));
}
function getLabelAtlasMaxTextureSize(options = {}) {
const rendererMaxTextureSize = sanitizePositiveInteger(
options.maxTextureSize,
16384
);
const canvasMaxTextureSize = sanitizePositiveInteger(
options.maxCanvasTextureSize,
DEFAULT_MAX_LABEL_CANVAS_SIZE
);
return Math.min(rendererMaxTextureSize, canvasMaxTextureSize);
}
function getNodeColorComponents(node) {
if (node?.color) {
LABEL_NODE_COLOR.set(node.color);
return [LABEL_NODE_COLOR.r, LABEL_NODE_COLOR.g, LABEL_NODE_COLOR.b];
}
return [1, 1, 1];
}
function compareSelectionCandidates(a, b) {
if (b.hasManualPriority !== a.hasManualPriority) {
return Number(b.hasManualPriority) - Number(a.hasManualPriority);
}
if (b.manualPriority !== a.manualPriority) {
return b.manualPriority - a.manualPriority;
}
if (b.degree !== a.degree) {
return b.degree - a.degree;
}
return a.entry.stableId - b.entry.stableId;
}
function relaxGraphDistances(sourceIndex, adjacency, distances, maxHops) {
if (!Number.isInteger(sourceIndex) || sourceIndex < 0) {
return;
}
const visited = new Int16Array(adjacency.length || distances.length);
visited.fill(-1);
const queue = [sourceIndex];
const depths = [0];
visited[sourceIndex] = 0;
distances[sourceIndex] = 0;
for (let i = 0; i < queue.length; i++) {
const nodeIndex = queue[i];
const depth = depths[i];
if (depth >= maxHops) {
continue;
}
const neighbors = adjacency[nodeIndex] || [];
for (let j = 0; j < neighbors.length; j++) {
const neighbor = neighbors[j];
if (!Number.isInteger(neighbor) || neighbor < 0 || neighbor >= visited.length) {
continue;
}
if (visited[neighbor] >= 0) {
continue;
}
const nextDepth = depth + 1;
visited[neighbor] = nextDepth;
if (nextDepth < distances[neighbor]) {
distances[neighbor] = nextDepth;
}
queue.push(neighbor);
depths.push(nextDepth);
}
}
}
function buildLabelSelectionOrder(entries, adjacency = [], nodes = [], degrees = [], maxHops = LABEL_GRAPH_DISTANCE_HOPS) {
if (!entries || entries.length === 0) {
return [];
}
const candidates = entries.map((entry) => {
const node = nodes[entry.nodeIndex] || {};
const manualPriority = typeof node.labelPriority === "number" && Number.isFinite(node.labelPriority) ? node.labelPriority : -Infinity;
return {
entry,
degree: typeof degrees[entry.nodeIndex] === "number" && Number.isFinite(degrees[entry.nodeIndex]) ? degrees[entry.nodeIndex] : 0,
hasManualPriority: Number.isFinite(manualPriority),
manualPriority,
selected: false
};
}).sort(compareSelectionCandidates);
const distances = new Int16Array(
Math.max(
1,
nodes.length,
adjacency.length,
...entries.map((entry) => entry.nodeIndex + 1)
)
);
distances.fill(maxHops + 1);
const order = [];
let hasSelection = false;
for (let threshold = maxHops + 1; threshold >= 0; threshold--) {
for (let i = 0; i < candidates.length; i++) {
const candidate = candidates[i];
if (candidate.selected) {
continue;
}
const distance = distances[candidate.entry.nodeIndex];
if (hasSelection && distance < threshold) {
continue;
}
candidate.selected = true;
order.push(candidate.entry);
relaxGraphDistances(
candidate.entry.nodeIndex,
adjacency,
distances,
maxHops
);
hasSelection = true;
}
}
return order;
}
function layoutAtlasRows(items, maxTextureSize) {
if (!Number.isFinite(maxTextureSize) || maxTextureSize <= 0) {
return { fits: false, width: 0, height: 0, placements: [] };
}
let x = 0;
let y = 0;
let rowHeight = 0;
let maxWidth = 0;
const placements = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const width = Math.max(1, Math.ceil(item.labelWidth));
const height = Math.max(1, Math.ceil(item.labelHeight));
if (width > maxTextureSize || height > maxTextureSize) {
return { fits: false, width: 0, height: 0, placements: [] };
}
if (x > 0 && x + width > maxTextureSize) {
x = 0;
y += rowHeight;
rowHeight = 0;
}
placements.push({
x,
y,
width,
height
});
x += width;
rowHeight = Math.max(rowHeight, height);
maxWidth = Math.max(maxWidth, x);
if (y + rowHeight > maxTextureSize) {
return { fits: false, width: 0, height: 0, placements: [] };
}
}
return {
fits: true,
width: Math.max(1, maxWidth),
height: Math.max(1, y + rowHeight),
placements
};
}
function measureAtlasCandidate(tempCtx, rawItems, { requestedFontSize, requestedPadding, fontFamily, scale, maxTextureSize }) {
const padding = Math.max(1, Math.round(requestedPadding * scale));
const fontSize = Math.max(1, Math.round(requestedFontSize * scale));
const tileH = fontSize + padding * 2;
if (tileH > maxTextureSize) {
return { fits: false };
}
tempCtx.font = `${fontSize}px ${fontFamily}`;
const items = rawItems.map((item) => {
const labelWidth = Math.ceil(tempCtx.measureText(item.text).width) + padding * 2;
return {
...item,
labelWidth,
labelHeight: tileH,
aspectRatio: labelWidth / tileH
};
});
const layout = layoutAtlasRows(items, maxTextureSize);
return {
fits: layout.fits,
padding,
fontSize,
tileH,
items,
layout
};
}
function fitAtlasLayout(tempCtx, rawItems, { requestedFontSize, requestedPadding, fontFamily, maxTextureSize }) {
let lo = 0;
let hi = 1;
let best = null;
for (let i = 0; i < 12; i++) {
const scale = (lo + hi) * 0.5;
const candidate = measureAtlasCandidate(tempCtx, rawItems, {
requestedFontSize,
requestedPadding,
fontFamily,
scale,
maxTextureSize
});
if (candidate.fits) {
best = {
...candidate,
scale
};
lo = scale;
} else {
hi = scale;
}
}
if (best) {
return best;
}
return measureAtlasCandidate(tempCtx, rawItems, {
requestedFontSize,
requestedPadding,
fontFamily,
scale: 0.01,
maxTextureSize
});
}
function buildTextAtlas(nodes, degrees = [], options = {}) {
const fontScale = sanitizeLabelFontSize(options.fontSize);
const atlasScale = ATLAS_RASTER_SCALE;
const requestedPadding = Math.max(
1,
Math.round(BASE_ATLAS_PADDING * fontScale * atlasScale)
);
const requestedFontSize = Math.max(
1,
Math.round(BASE_ATLAS_FONT_SIZE * fontScale * atlasScale)
);
const fontFamily = options.fontFamily || DEFAULT_FONT_FAMILY;
const maxTextureSize = getLabelAtlasMaxTextureSize(options);
const textColor = "#fff";
const temp = document.createElement("canvas");
const tempCtx = temp.getContext("2d");
tempCtx.font = `${requestedFontSize}px ${fontFamily}`;
const rawItems = [];
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
if (node.label === null || node.label === void 0) {
continue;
}
const text = String(node.label);
rawItems.push({
text,
nodeIndex: i,
pointSize: typeof node.size === "number" && Number.isFinite(node.size) ? node.size : 1,
basePriority: getLabelBasePriority(node, degrees[i] || 0)
});
}
if (rawItems.length === 0) {
return null;
}
const fittedAtlas = fitAtlasLayout(tempCtx, rawItems, {
requestedFontSize,
requestedPadding,
fontFamily,
maxTextureSize
});
if (!fittedAtlas.fits) {
return null;
}
const canvas = document.createElement("canvas");
canvas.width = fittedAtlas.layout.width;
canvas.height = fittedAtlas.layout.height;
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = true;
ctx.font = `${fittedAtlas.fontSize}px ${fontFamily}`;
ctx.fillStyle = textColor;
ctx.textBaseline = "middle";
ctx.textAlign = "center";
const entries = [];
for (let i = 0; i < fittedAtlas.items.length; i++) {
const item = fittedAtlas.items[i];
const placement = fittedAtlas.layout.placements[i];
const px = placement.x;
const py = placement.y;
ctx.fillText(
item.text,
px + placement.width / 2,
py + placement.height / 2
);
entries.push({
...item,
labelId: i,
stableId: item.nodeIndex,
persistence: 0,
atlasUV: {
u: px / canvas.width,
v: 1 - (py + placement.height) / canvas.height,
uw: placement.width / canvas.width,
uh: placement.height / canvas.height
}
});
}
return { canvas, entries };
}
function getLabelBasePriority(node, degree = 0) {
if (typeof node.labelPriority === "number" && Number.isFinite(node.labelPriority)) {
return node.labelPriority;
}
if (typeof node.size === "number" && Number.isFinite(node.size)) {
return node.size;
}
if (Number.isFinite(degree)) {
return degree;
}
return 0;
}
function compareLabelEntries(a, b) {
if (b.basePriority !== a.basePriority) {
return b.basePriority - a.basePriority;
}
return a.stableId - b.stableId;
}
function buildSelectionRanks(entries, selectionOrder) {
const ranksByLabelId = new Float32Array(entries.length);
const orderedEntries = selectionOrder && selectionOrder.length > 0 ? selectionOrder : entries.slice().sort(compareLabelEntries);
for (let i = 0; i < orderedEntries.length; i++) {
ranksByLabelId[orderedEntries[i].labelId] = i;
}
return ranksByLabelId;
}
function configureAtlasTexture(texture, options = {}) {
const useMipmaps = Boolean(options.useMipmaps);
texture.minFilter = useMipmaps ? import_three4.LinearMipmapLinearFilter : import_three4.LinearFilter;
texture.magFilter = import_three4.LinearFilter;
texture.wrapS = import_three4.ClampToEdgeWrapping;
texture.wrapT = import_three4.ClampToEdgeWrapping;
texture.generateMipmaps = useMipmaps;
texture.needsUpdate = true;
return texture;
}
var Labels = class extends import_three4.Mesh {
constructor({ geometry, texture, entries, fontFamily }, uniforms) {
const material = new import_three4.ShaderMaterial({
uniforms: {
...import_three4.UniformsLib.fog,
...{
texturePositions: { value: null },
textureAtlas: { value: texture },
opacity: uniforms.opacity,
obscurity: uniforms.obscurity,
frustumSize: uniforms.frustumSize,
inheritColors: uniforms.labelsInheritColor,
is2D: uniforms.is2D,
sizeAttenuation: uniforms.sizeAttenuation,
resolution: uniforms.resolution,
nodeRadius: uniforms.nodeRadius,
nodeScale: uniforms.nodeScale,
uLabelCount: { value: entries.length },
uColor: uniforms.labelColor,
labelAlignment: uniforms.labelAlignment,
labelBaseline: uniforms.labelBaseline,
labelFontSize: uniforms.labelFontSize,
labelNear: uniforms.labelNear,
labelOffset: uniforms.labelOffset,
uBeginning: uniforms.uBeginning,
uEnding: uniforms.uEnding,
uNodeAmount: uniforms.uNodeAmount
}
},
vertexShader: labels_default.vertexShader,
fragmentShader: labels_default.fragmentShader,
transparent: true,
vertexColors: true,
depthWrite: false,
depthTest: false,
fog: true
});
super(geometry, material);
this.frustumCulled = false;
this.entries = entries;
this.userData.fontFamily = fontFamily || DEFAULT_FONT_FAMILY;
this.userData.fontSize = uniforms.labelFontSize.value;
this.userData.near = sanitizeLabelNearDistance(uniforms.labelNear.value);
}
dispose() {
this.material.uniforms.textureAtlas.value?.dispose?.();
this.material.dispose();
this.geometry.dispose();
}
replaceData({
geometry,
texture,
entries,
fontFamily,
fontSize
}) {
this.geometry.dispose();
this.material.uniforms.textureAtlas.value?.dispose?.();
this.geometry = geometry;
this.entries = entries;
this.material.uniforms.textureAtlas.value = texture;
this.material.uniforms.uLabelCount.value = entries.length;
this.userData.fontFamily = fontFamily || DEFAULT_FONT_FAMILY;
this.userData.fontSize = sanitizeLabelFontSize(fontSize);
this.userData.near = sanitizeLabelNearDistance(
this.material.uniforms.labelNear.value
);
}
get fontSize() {
if (this.parent?.userData?.uniforms?.labelFontSize) {
return this.parent.userData.uniforms.labelFontSize.value;
}
return this.userData.fontSize;
}
set fontSize(v) {
const nextValue = sanitizeLabelFontSize(v);
this.userData.fontSize = nextValue;
if (!this.material?.uniforms?.labelFontSize) {
return;
}
if (this.material.uniforms.labelFontSize.value === nextValue) {
return;
}
this.material.uniforms.labelFontSize.value = nextValue;
}
get fontFamily() {
if (this.parent?.userData?.labelFontFamily) {
return this.parent.userData.labelFontFamily;
}
return this.userData.fontFamily;
}
set fontFamily(v) {
const nextValue = typeof v === "string" && v.trim().length > 0 ? v.trim() : DEFAULT_FONT_FAMILY;
this.userData.fontFamily = nextValue;
if (!this.parent?.userData) {
return;
}
if (this.parent.userData.labelFontFamily === nextValue) {
return;
}
this.parent.userData.labelFontFamily = nextValue;
this.parent.refreshLabels();
}
get alignment() {
return getLabelAlignmentName(this.material.uniforms.labelAlignment.value);
}
set alignment(v) {
this.material.uniforms.labelAlignment.value = LabelAlignmentMap[v] ?? LabelAlignmentMap.center;
}
get baseline() {
return getLabelBaselineName(this.material.uniforms.labelBaseline.value);
}
set baseline(v) {
this.material.uniforms.labelBaseline.value = LabelBaselineMap[v] ?? LabelBaselineMap.top;
}
get offset() {
return this.material.uniforms.labelOffset.value;
}
set offset(v) {
if (!v || !Number.isFinite(v.x) || !Number.isFinite(v.y)) {
return;
}
this.material.uniforms.labelOffset.value.set(v.x, v.y);
}
get near() {
if (this.parent?.userData?.uniforms?.labelNear) {
return this.parent.userData.uniforms.labelNear.value;
}
return this.userData.near;
}
set near(v) {
const nextValue = sanitizeLabelNearDistance(v);
this.userData.near = nextValue;
if (!this.material?.uniforms?.labelNear) {
return;
}
if (this.material.uniforms.labelNear.value === nextValue) {
return;
}
this.material.uniforms.labelNear.value = nextValue;
}
static parse(size2, data, options = {}) {
const atlas = buildTextAtlas(data.nodes, options.degrees || [], options);
if (!atlas) {
return Promise.resolve(null);
}
const { canvas, entries } = atlas;
const quadVerts = new Float32Array([
-1,
-1,
0,
1,
-1,
0,
-1,
1,
0,
1,
1,
0
]);
const quadUVs = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
const quadIdx = [0, 1, 2, 2, 1, 3];
const geometry = new import_three4.InstancedBufferGeometry();
geometry.setAttribute("position", new import_three4.BufferAttribute(quadVerts, 3));
geometry.setAttribute("uv", new import_three4.Bu