hyper-scatter 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1404 @@
1
+ import { DEFAULT_COLORS, HOVER_COLOR, SELECTION_COLOR, } from "../core/types.js";
2
+ import { pointInPolygon } from "../core/selection/point_in_polygon.js";
3
+ import { createDefaultOrbitView3D, createIndicesSelectionResult3D, } from "../core/types3d.js";
4
+ const WORLD_UP = [0, 1, 0];
5
+ const MAX_SELECTION_RENDER_POINTS = 250_000;
6
+ function clamp(v, lo, hi) {
7
+ if (v < lo)
8
+ return lo;
9
+ if (v > hi)
10
+ return hi;
11
+ return v;
12
+ }
13
+ function createMat4Identity() {
14
+ return new Float32Array([
15
+ 1, 0, 0, 0,
16
+ 0, 1, 0, 0,
17
+ 0, 0, 1, 0,
18
+ 0, 0, 0, 1,
19
+ ]);
20
+ }
21
+ function mat4Multiply(a, b) {
22
+ const out = new Float32Array(16);
23
+ for (let c = 0; c < 4; c++) {
24
+ for (let r = 0; r < 4; r++) {
25
+ out[c * 4 + r] =
26
+ a[0 * 4 + r] * b[c * 4 + 0] +
27
+ a[1 * 4 + r] * b[c * 4 + 1] +
28
+ a[2 * 4 + r] * b[c * 4 + 2] +
29
+ a[3 * 4 + r] * b[c * 4 + 3];
30
+ }
31
+ }
32
+ return out;
33
+ }
34
+ function vec3Length(v) {
35
+ return Math.hypot(v[0], v[1], v[2]);
36
+ }
37
+ function vec3Normalize(v) {
38
+ const len = vec3Length(v);
39
+ if (len < 1e-12)
40
+ return [0, 0, 0];
41
+ return [v[0] / len, v[1] / len, v[2] / len];
42
+ }
43
+ function vec3Sub(a, b) {
44
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
45
+ }
46
+ function vec3Add(a, b) {
47
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
48
+ }
49
+ function vec3Scale(v, s) {
50
+ return [v[0] * s, v[1] * s, v[2] * s];
51
+ }
52
+ function vec3Cross(a, b) {
53
+ return [
54
+ a[1] * b[2] - a[2] * b[1],
55
+ a[2] * b[0] - a[0] * b[2],
56
+ a[0] * b[1] - a[1] * b[0],
57
+ ];
58
+ }
59
+ function mat4LookAt(eye, target, up) {
60
+ const zAxis = vec3Normalize(vec3Sub(eye, target));
61
+ let xAxis = vec3Normalize(vec3Cross(up, zAxis));
62
+ if (vec3Length(xAxis) < 1e-9) {
63
+ xAxis = [1, 0, 0];
64
+ }
65
+ const yAxis = vec3Cross(zAxis, xAxis);
66
+ const out = createMat4Identity();
67
+ out[0] = xAxis[0];
68
+ out[1] = yAxis[0];
69
+ out[2] = zAxis[0];
70
+ out[3] = 0;
71
+ out[4] = xAxis[1];
72
+ out[5] = yAxis[1];
73
+ out[6] = zAxis[1];
74
+ out[7] = 0;
75
+ out[8] = xAxis[2];
76
+ out[9] = yAxis[2];
77
+ out[10] = zAxis[2];
78
+ out[11] = 0;
79
+ out[12] = -(xAxis[0] * eye[0] + xAxis[1] * eye[1] + xAxis[2] * eye[2]);
80
+ out[13] = -(yAxis[0] * eye[0] + yAxis[1] * eye[1] + yAxis[2] * eye[2]);
81
+ out[14] = -(zAxis[0] * eye[0] + zAxis[1] * eye[1] + zAxis[2] * eye[2]);
82
+ out[15] = 1;
83
+ return out;
84
+ }
85
+ function mat4Ortho(left, right, bottom, top, near, far) {
86
+ const out = new Float32Array(16);
87
+ out[0] = 2 / (right - left);
88
+ out[5] = 2 / (top - bottom);
89
+ out[10] = -2 / (far - near);
90
+ out[12] = -(right + left) / (right - left);
91
+ out[13] = -(top + bottom) / (top - bottom);
92
+ out[14] = -(far + near) / (far - near);
93
+ out[15] = 1;
94
+ return out;
95
+ }
96
+ function transformClip(m, x, y, z) {
97
+ return [
98
+ m[0] * x + m[4] * y + m[8] * z + m[12],
99
+ m[1] * x + m[5] * y + m[9] * z + m[13],
100
+ m[2] * x + m[6] * y + m[10] * z + m[14],
101
+ m[3] * x + m[7] * y + m[11] * z + m[15],
102
+ ];
103
+ }
104
+ function parseHexColor(color) {
105
+ const s = color.trim();
106
+ if (!s.startsWith("#"))
107
+ return [1, 1, 1, 1];
108
+ const hex = s.slice(1);
109
+ if (hex.length === 3) {
110
+ const r = Number.parseInt(hex[0] + hex[0], 16) / 255;
111
+ const g = Number.parseInt(hex[1] + hex[1], 16) / 255;
112
+ const b = Number.parseInt(hex[2] + hex[2], 16) / 255;
113
+ return [r, g, b, 1];
114
+ }
115
+ if (hex.length === 6 || hex.length === 8) {
116
+ const r = Number.parseInt(hex.slice(0, 2), 16) / 255;
117
+ const g = Number.parseInt(hex.slice(2, 4), 16) / 255;
118
+ const b = Number.parseInt(hex.slice(4, 6), 16) / 255;
119
+ const a = hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) / 255 : 1;
120
+ return [r, g, b, a];
121
+ }
122
+ return [1, 1, 1, 1];
123
+ }
124
+ function parseHexColorBytes(color) {
125
+ const s = color.trim();
126
+ if (!s.startsWith("#"))
127
+ return [255, 255, 255, 255];
128
+ const hex = s.slice(1);
129
+ if (hex.length === 3) {
130
+ return [
131
+ Number.parseInt(hex[0] + hex[0], 16),
132
+ Number.parseInt(hex[1] + hex[1], 16),
133
+ Number.parseInt(hex[2] + hex[2], 16),
134
+ 255,
135
+ ];
136
+ }
137
+ if (hex.length === 6 || hex.length === 8) {
138
+ return [
139
+ Number.parseInt(hex.slice(0, 2), 16),
140
+ Number.parseInt(hex.slice(2, 4), 16),
141
+ Number.parseInt(hex.slice(4, 6), 16),
142
+ hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) : 255,
143
+ ];
144
+ }
145
+ return [255, 255, 255, 255];
146
+ }
147
+ function compileShader(gl, type, source) {
148
+ const shader = gl.createShader(type);
149
+ if (!shader)
150
+ throw new Error("Failed to create shader");
151
+ gl.shaderSource(shader, source);
152
+ gl.compileShader(shader);
153
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
154
+ const info = gl.getShaderInfoLog(shader) ?? "unknown";
155
+ gl.deleteShader(shader);
156
+ throw new Error(`Shader compile failed: ${info}`);
157
+ }
158
+ return shader;
159
+ }
160
+ function linkProgram(gl, vsSource, fsSource) {
161
+ const vs = compileShader(gl, gl.VERTEX_SHADER, vsSource);
162
+ const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
163
+ const program = gl.createProgram();
164
+ if (!program)
165
+ throw new Error("Failed to create program");
166
+ gl.attachShader(program, vs);
167
+ gl.attachShader(program, fs);
168
+ gl.linkProgram(program);
169
+ gl.deleteShader(vs);
170
+ gl.deleteShader(fs);
171
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
172
+ const info = gl.getProgramInfoLog(program) ?? "unknown";
173
+ gl.deleteProgram(program);
174
+ throw new Error(`Program link failed: ${info}`);
175
+ }
176
+ return program;
177
+ }
178
+ function setCanvasSize(canvas, width, height, dpr) {
179
+ canvas.width = Math.max(1, Math.floor(width * dpr));
180
+ canvas.height = Math.max(1, Math.floor(height * dpr));
181
+ canvas.style.width = `${width}px`;
182
+ canvas.style.height = `${height}px`;
183
+ }
184
+ function createSelectionResult(indices, computeTimeMs) {
185
+ return createIndicesSelectionResult3D(indices, computeTimeMs);
186
+ }
187
+ const VS_POINTS_3D = `#version 300 es
188
+ precision highp float;
189
+ precision highp int;
190
+
191
+ layout(location = 0) in vec3 a_pos;
192
+ layout(location = 1) in uint a_label;
193
+
194
+ uniform mat4 u_mvp;
195
+ uniform float u_pointSizePx;
196
+
197
+ flat out uint v_label;
198
+
199
+ void main() {
200
+ gl_Position = u_mvp * vec4(a_pos, 1.0);
201
+ gl_PointSize = u_pointSizePx;
202
+ v_label = a_label;
203
+ }
204
+ `;
205
+ const FS_POINTS_3D = `#version 300 es
206
+ precision highp float;
207
+ precision highp int;
208
+
209
+ flat in uint v_label;
210
+
211
+ uniform sampler2D u_paletteTex;
212
+ uniform int u_paletteSize;
213
+ uniform int u_paletteWidth;
214
+
215
+ out vec4 outColor;
216
+
217
+ void main() {
218
+ vec2 p = gl_PointCoord * 2.0 - 1.0;
219
+ float r = length(p);
220
+ float aa = max(fwidth(r), 0.02);
221
+ float alpha = 1.0 - smoothstep(1.0 - aa, 1.0 + aa, r);
222
+ if (alpha <= 0.0) discard;
223
+
224
+ int size = max(u_paletteSize, 1);
225
+ int w = max(u_paletteWidth, 1);
226
+ int idx = int(v_label) % size;
227
+ int x = idx % w;
228
+ int y = idx / w;
229
+ vec4 c = texelFetch(u_paletteTex, ivec2(x, y), 0);
230
+ float outAlpha = c.a * alpha;
231
+ if (outAlpha <= 0.0) discard;
232
+ outColor = vec4(c.rgb, outAlpha);
233
+ }
234
+ `;
235
+ const VS_SOLID_3D = `#version 300 es
236
+ precision highp float;
237
+
238
+ layout(location = 0) in vec3 a_pos;
239
+
240
+ uniform mat4 u_mvp;
241
+ uniform float u_pointSizePx;
242
+
243
+ void main() {
244
+ gl_Position = u_mvp * vec4(a_pos, 1.0);
245
+ gl_PointSize = u_pointSizePx;
246
+ }
247
+ `;
248
+ const FS_SOLID_3D = `#version 300 es
249
+ precision highp float;
250
+
251
+ uniform vec4 u_color;
252
+ uniform float u_pointSizePx;
253
+ uniform int u_ringMode;
254
+ uniform float u_ringThicknessPx;
255
+
256
+ out vec4 outColor;
257
+
258
+ void main() {
259
+ vec2 p = gl_PointCoord * 2.0 - 1.0;
260
+ float r = length(p);
261
+ float radiusPx = max(u_pointSizePx * 0.5, 1.0);
262
+ float aa = max(fwidth(r), 1.2 / radiusPx);
263
+ float outer = 1.0 - smoothstep(1.0 - aa, 1.0 + aa, r);
264
+ if (outer <= 0.0) discard;
265
+
266
+ float alpha = outer;
267
+
268
+ if (u_ringMode == 1) {
269
+ float t = clamp(u_ringThicknessPx / max(radiusPx, 1e-6), 0.0, 1.0);
270
+ float inner = 1.0 - t;
271
+ float innerMask = smoothstep(inner - aa, inner + aa, r);
272
+ alpha *= innerMask;
273
+ if (alpha <= 0.0) discard;
274
+ }
275
+
276
+ outColor = vec4(u_color.rgb, u_color.a * alpha);
277
+ }
278
+ `;
279
+ const VS_GUIDE_3D = `#version 300 es
280
+ precision highp float;
281
+
282
+ layout(location = 0) in vec3 a_pos;
283
+ uniform mat4 u_mvp;
284
+
285
+ void main() {
286
+ gl_Position = u_mvp * vec4(a_pos, 1.0);
287
+ }
288
+ `;
289
+ const FS_GUIDE_3D = `#version 300 es
290
+ precision highp float;
291
+
292
+ uniform vec4 u_color;
293
+ out vec4 outColor;
294
+
295
+ void main() {
296
+ outColor = u_color;
297
+ }
298
+ `;
299
+ class PointCloud3DWebGLBase {
300
+ canvas = null;
301
+ gl = null;
302
+ width = 0;
303
+ height = 0;
304
+ dpr = 1;
305
+ dataset = null;
306
+ view = createDefaultOrbitView3D();
307
+ sceneRadius = 1;
308
+ backgroundColor = "#0a0a0a";
309
+ pointRadiusCss = 4;
310
+ colors = DEFAULT_COLORS;
311
+ sphereGuideColor = "#94a3b8";
312
+ sphereGuideOpacity = 0.2;
313
+ categoryVisibilityMask = new Uint8Array(0);
314
+ hasCategoryVisibilityMask = false;
315
+ categoryAlpha = 1;
316
+ interactionStyle = {
317
+ selectionColor: SELECTION_COLOR,
318
+ hoverColor: HOVER_COLOR,
319
+ hoverFillColor: null,
320
+ };
321
+ selection = new Set();
322
+ hoveredIndex = -1;
323
+ pointsProgram = null;
324
+ solidProgram = null;
325
+ guideProgram = null;
326
+ pointsVao = null;
327
+ pointsPosBuffer = null;
328
+ pointsLabelBuffer = null;
329
+ selectionVao = null;
330
+ selectionPosBuffer = null;
331
+ selectionVertexCount = 0;
332
+ hoverVao = null;
333
+ hoverPosBuffer = null;
334
+ hoverVertexCount = 0;
335
+ guideVao = null;
336
+ guideBuffer = null;
337
+ guideVertexCount = 0;
338
+ guideSegmentVerts = 0;
339
+ guideAxisVertexOffset = 0;
340
+ guideAxisVertexCount = 0;
341
+ paletteTex = null;
342
+ paletteSize = 0;
343
+ paletteWidth = 0;
344
+ paletteHeight = 0;
345
+ paletteBytes = new Uint8Array(0);
346
+ paletteTexUnit = 1;
347
+ mvpMatrix = createMat4Identity();
348
+ projectedDirty = true;
349
+ selectionDirty = true;
350
+ hoverDirty = true;
351
+ projectedScreenX = new Float32Array(0);
352
+ projectedScreenY = new Float32Array(0);
353
+ projectedDepth = new Float32Array(0);
354
+ projectedPixelIndex = new Int32Array(0);
355
+ projectedVisible = new Uint8Array(0);
356
+ projectedVisibleIndices = new Uint32Array(0);
357
+ projectedVisibleCount = 0;
358
+ depthBuffer = new Float32Array(0);
359
+ selectionPositionsScratch = new Float32Array(0);
360
+ hoverPositionScratch = new Float32Array(3);
361
+ supportsSphereGuide() {
362
+ return false;
363
+ }
364
+ supportsEuclideanGuide() {
365
+ return false;
366
+ }
367
+ preprocessPositions(input) {
368
+ return input;
369
+ }
370
+ init(canvas, opts) {
371
+ this.canvas = canvas;
372
+ this.width = opts.width;
373
+ this.height = opts.height;
374
+ this.dpr = opts.devicePixelRatio ?? window.devicePixelRatio ?? 1;
375
+ if (opts.backgroundColor)
376
+ this.backgroundColor = opts.backgroundColor;
377
+ if (typeof opts.pointRadius === "number" && Number.isFinite(opts.pointRadius)) {
378
+ this.pointRadiusCss = Math.max(1, opts.pointRadius);
379
+ }
380
+ if (opts.colors?.length)
381
+ this.colors = opts.colors;
382
+ if (opts.sphereGuideColor)
383
+ this.sphereGuideColor = opts.sphereGuideColor;
384
+ if (typeof opts.sphereGuideOpacity === "number" && Number.isFinite(opts.sphereGuideOpacity)) {
385
+ this.sphereGuideOpacity = clamp(opts.sphereGuideOpacity, 0, 1);
386
+ }
387
+ setCanvasSize(canvas, this.width, this.height, this.dpr);
388
+ this.projectedDirty = true;
389
+ this.selectionDirty = true;
390
+ this.hoverDirty = true;
391
+ }
392
+ setDataset(dataset) {
393
+ if (dataset.geometry !== this.expectedGeometry()) {
394
+ throw new Error(`Expected geometry '${this.expectedGeometry()}', got '${dataset.geometry}'`);
395
+ }
396
+ const processedPositions = this.preprocessPositions(dataset.positions);
397
+ this.dataset = {
398
+ n: dataset.n,
399
+ positions: processedPositions,
400
+ labels: dataset.labels,
401
+ geometry: dataset.geometry,
402
+ };
403
+ this.selection = new Set();
404
+ this.hoveredIndex = -1;
405
+ this.fitViewToDataset();
406
+ this.ensureProjectedCapacity(this.dataset.n);
407
+ this.selectionDirty = true;
408
+ this.hoverDirty = true;
409
+ this.projectedDirty = true;
410
+ if (this.gl) {
411
+ this.uploadDatasetToGPU();
412
+ this.rebuildGuideGeometry();
413
+ this.uploadSelectionToGPU();
414
+ this.uploadHoverToGPU();
415
+ }
416
+ }
417
+ setPalette(colors) {
418
+ this.colors = colors;
419
+ if (this.gl) {
420
+ this.uploadPalette();
421
+ }
422
+ }
423
+ setCategoryVisibility(mask) {
424
+ if (mask == null) {
425
+ this.categoryVisibilityMask = new Uint8Array(0);
426
+ this.hasCategoryVisibilityMask = false;
427
+ }
428
+ else {
429
+ const n = mask.length >>> 0;
430
+ const next = new Uint8Array(n);
431
+ for (let i = 0; i < n; i++) {
432
+ next[i] = mask[i] ? 1 : 0;
433
+ }
434
+ this.categoryVisibilityMask = next;
435
+ this.hasCategoryVisibilityMask = true;
436
+ }
437
+ if (this.hoveredIndex >= 0 && !this.isPointVisibleByCategory(this.hoveredIndex)) {
438
+ this.hoveredIndex = -1;
439
+ }
440
+ this.selectionDirty = true;
441
+ this.hoverDirty = true;
442
+ this.projectedDirty = true;
443
+ if (this.gl) {
444
+ this.uploadPalette();
445
+ this.uploadSelectionToGPU();
446
+ this.uploadHoverToGPU();
447
+ }
448
+ }
449
+ setCategoryAlpha(alpha) {
450
+ const next = Number.isFinite(alpha) ? clamp(alpha, 0, 1) : 1;
451
+ if (Math.abs(next - this.categoryAlpha) <= 1e-12)
452
+ return;
453
+ this.categoryAlpha = next;
454
+ if (this.gl) {
455
+ this.uploadPalette();
456
+ }
457
+ }
458
+ setInteractionStyle(style) {
459
+ if (typeof style.selectionColor === "string" && style.selectionColor.length > 0) {
460
+ this.interactionStyle.selectionColor = style.selectionColor;
461
+ }
462
+ if (typeof style.hoverColor === "string" && style.hoverColor.length > 0) {
463
+ this.interactionStyle.hoverColor = style.hoverColor;
464
+ }
465
+ if (Object.prototype.hasOwnProperty.call(style, "hoverFillColor")) {
466
+ this.interactionStyle.hoverFillColor = style.hoverFillColor ?? null;
467
+ }
468
+ }
469
+ isCategoryVisible(category) {
470
+ if (!this.hasCategoryVisibilityMask)
471
+ return true;
472
+ const mask = this.categoryVisibilityMask;
473
+ if (category < 0 || category >= mask.length)
474
+ return true;
475
+ return mask[category] !== 0;
476
+ }
477
+ isPointVisibleByCategory(index) {
478
+ const ds = this.dataset;
479
+ if (!ds || index < 0 || index >= ds.n)
480
+ return false;
481
+ return this.isCategoryVisible(ds.labels[index]);
482
+ }
483
+ setView(view) {
484
+ this.view = { ...view };
485
+ this.projectedDirty = true;
486
+ }
487
+ getView() {
488
+ return { ...this.view };
489
+ }
490
+ resize(width, height) {
491
+ this.width = width;
492
+ this.height = height;
493
+ if (this.canvas) {
494
+ setCanvasSize(this.canvas, width, height, this.dpr);
495
+ }
496
+ if (this.gl && this.canvas) {
497
+ this.gl.viewport(0, 0, this.canvas.width, this.canvas.height);
498
+ }
499
+ this.projectedDirty = true;
500
+ }
501
+ setSelection(indices) {
502
+ // Keep selection state encapsulated: never retain caller-owned Set references.
503
+ this.selection = new Set(indices);
504
+ this.selectionDirty = true;
505
+ }
506
+ getSelection() {
507
+ return new Set(this.selection);
508
+ }
509
+ setHovered(index) {
510
+ if (index >= 0 && !this.isPointVisibleByCategory(index)) {
511
+ this.hoveredIndex = -1;
512
+ }
513
+ else {
514
+ this.hoveredIndex = index;
515
+ }
516
+ this.hoverDirty = true;
517
+ }
518
+ pan(deltaX, deltaY, modifiers) {
519
+ if (modifiers.meta || modifiers.ctrl || modifiers.alt) {
520
+ const { right, up } = this.getCameraFrame();
521
+ const scale = (2 * this.view.orthoScale) / Math.max(1, Math.min(this.width, this.height));
522
+ const tx = -deltaX * scale;
523
+ const ty = deltaY * scale;
524
+ this.view.targetX += right[0] * tx + up[0] * ty;
525
+ this.view.targetY += right[1] * tx + up[1] * ty;
526
+ this.view.targetZ += right[2] * tx + up[2] * ty;
527
+ }
528
+ else {
529
+ this.view.yaw -= deltaX * 0.005;
530
+ this.view.pitch = clamp(this.view.pitch - deltaY * 0.005, -1.45, 1.45);
531
+ }
532
+ this.projectedDirty = true;
533
+ }
534
+ zoom(_anchorX, _anchorY, delta, _modifiers) {
535
+ const factor = Math.pow(1.1, -delta);
536
+ this.view.orthoScale = clamp(this.view.orthoScale * factor, 0.02, 10000);
537
+ this.view.distance = clamp(this.view.distance * factor, 0.05, 10000);
538
+ this.projectedDirty = true;
539
+ }
540
+ hitTest(screenX, screenY) {
541
+ const ds = this.dataset;
542
+ if (!ds)
543
+ return null;
544
+ this.ensureProjectedCache();
545
+ const maxDist = this.pointRadiusCss + 5;
546
+ const maxDistSq = maxDist * maxDist;
547
+ let bestIndex = -1;
548
+ let bestDistSq = Infinity;
549
+ let bestDepth = Infinity;
550
+ for (let k = 0; k < this.projectedVisibleCount; k++) {
551
+ const i = this.projectedVisibleIndices[k];
552
+ if (!this.isCategoryVisible(ds.labels[i]))
553
+ continue;
554
+ const dx = this.projectedScreenX[i] - screenX;
555
+ const dy = this.projectedScreenY[i] - screenY;
556
+ const distSq = dx * dx + dy * dy;
557
+ if (distSq > maxDistSq)
558
+ continue;
559
+ const depth = this.projectedDepth[i];
560
+ if (distSq < bestDistSq ||
561
+ (Math.abs(distSq - bestDistSq) <= 1e-12 && depth < bestDepth) ||
562
+ (Math.abs(distSq - bestDistSq) <= 1e-12 && Math.abs(depth - bestDepth) <= 1e-12 && i < bestIndex)) {
563
+ bestIndex = i;
564
+ bestDistSq = distSq;
565
+ bestDepth = depth;
566
+ }
567
+ }
568
+ if (bestIndex < 0)
569
+ return null;
570
+ return {
571
+ index: bestIndex,
572
+ screenX: this.projectedScreenX[bestIndex],
573
+ screenY: this.projectedScreenY[bestIndex],
574
+ distance: Math.sqrt(bestDistSq),
575
+ depth: bestDepth,
576
+ };
577
+ }
578
+ lassoSelect(polyline) {
579
+ const ds = this.dataset;
580
+ if (!ds || polyline.length < 6) {
581
+ return createSelectionResult(new Set(), 0);
582
+ }
583
+ const startTime = performance.now();
584
+ this.ensureProjectedCache();
585
+ let minX = Infinity;
586
+ let minY = Infinity;
587
+ let maxX = -Infinity;
588
+ let maxY = -Infinity;
589
+ for (let i = 0; i < polyline.length; i += 2) {
590
+ const x = polyline[i];
591
+ const y = polyline[i + 1];
592
+ if (x < minX)
593
+ minX = x;
594
+ if (x > maxX)
595
+ maxX = x;
596
+ if (y < minY)
597
+ minY = y;
598
+ if (y > maxY)
599
+ maxY = y;
600
+ }
601
+ const indices = new Set();
602
+ for (let k = 0; k < this.projectedVisibleCount; k++) {
603
+ const i = this.projectedVisibleIndices[k];
604
+ if (!this.isCategoryVisible(ds.labels[i]))
605
+ continue;
606
+ const x = this.projectedScreenX[i];
607
+ const y = this.projectedScreenY[i];
608
+ if (x < minX || x > maxX || y < minY || y > maxY)
609
+ continue;
610
+ if (pointInPolygon(x, y, polyline)) {
611
+ indices.add(i);
612
+ }
613
+ }
614
+ return createSelectionResult(indices, performance.now() - startTime);
615
+ }
616
+ async countSelection(result, opts = {}) {
617
+ const indices = result.indices;
618
+ if (!indices)
619
+ return 0;
620
+ const shouldCancel = opts.shouldCancel;
621
+ const onProgress = opts.onProgress;
622
+ const yieldEveryMs = typeof opts.yieldEveryMs === "number" && Number.isFinite(opts.yieldEveryMs)
623
+ ? Math.max(0, opts.yieldEveryMs)
624
+ : 8;
625
+ let visibleCount = 0;
626
+ let processed = 0;
627
+ const CHECK_STRIDE = 16_384;
628
+ let nextCheck = CHECK_STRIDE;
629
+ let lastYieldTs = yieldEveryMs > 0 ? performance.now() : 0;
630
+ for (const i of indices) {
631
+ if (this.isPointVisibleByCategory(i))
632
+ visibleCount++;
633
+ processed++;
634
+ if (yieldEveryMs > 0 && processed >= nextCheck) {
635
+ nextCheck = processed + CHECK_STRIDE;
636
+ if (shouldCancel?.())
637
+ return visibleCount;
638
+ const now = performance.now();
639
+ if (now - lastYieldTs >= yieldEveryMs) {
640
+ onProgress?.(visibleCount, processed);
641
+ await new Promise((resolve) => requestAnimationFrame(() => resolve()));
642
+ lastYieldTs = performance.now();
643
+ }
644
+ }
645
+ }
646
+ onProgress?.(visibleCount, processed);
647
+ return visibleCount;
648
+ }
649
+ projectToScreen(dataX, dataY, dataZ) {
650
+ this.updateMvpMatrix();
651
+ const [clipX, clipY, clipZ, clipW] = transformClip(this.mvpMatrix, dataX, dataY, dataZ);
652
+ const invW = Math.abs(clipW) > 1e-12 ? 1 / clipW : 0;
653
+ const ndcX = clipX * invW;
654
+ const ndcY = clipY * invW;
655
+ const ndcZ = clipZ * invW;
656
+ const x = (ndcX * 0.5 + 0.5) * this.width;
657
+ const y = (1 - (ndcY * 0.5 + 0.5)) * this.height;
658
+ const depth = ndcZ * 0.5 + 0.5;
659
+ return {
660
+ x,
661
+ y,
662
+ depth,
663
+ visible: ndcX >= -1 && ndcX <= 1 && ndcY >= -1 && ndcY <= 1 && ndcZ >= -1 && ndcZ <= 1,
664
+ };
665
+ }
666
+ render() {
667
+ this.ensureGL();
668
+ const gl = this.gl;
669
+ const ds = this.dataset;
670
+ const canvas = this.canvas;
671
+ if (!gl || !ds || !canvas || !this.pointsProgram || !this.solidProgram)
672
+ return;
673
+ if (this.selectionDirty)
674
+ this.uploadSelectionToGPU();
675
+ if (this.hoverDirty)
676
+ this.uploadHoverToGPU();
677
+ this.updateMvpMatrix();
678
+ const [br, bg, bb, ba] = parseHexColor(this.backgroundColor);
679
+ gl.clearColor(br, bg, bb, ba);
680
+ gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
681
+ if (this.supportsSphereGuide()) {
682
+ this.drawSphereGuide();
683
+ }
684
+ else if (this.supportsEuclideanGuide()) {
685
+ this.drawEuclideanGuide();
686
+ }
687
+ gl.enable(gl.DEPTH_TEST);
688
+ gl.enable(gl.BLEND);
689
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
690
+ gl.useProgram(this.pointsProgram.program);
691
+ if (this.pointsProgram.uMvp)
692
+ gl.uniformMatrix4fv(this.pointsProgram.uMvp, false, this.mvpMatrix);
693
+ if (this.pointsProgram.uPointSizePx)
694
+ gl.uniform1f(this.pointsProgram.uPointSizePx, this.pointRadiusCss * 2 * this.dpr);
695
+ this.bindPaletteTexture();
696
+ if (this.pointsProgram.uPaletteTex)
697
+ gl.uniform1i(this.pointsProgram.uPaletteTex, this.paletteTexUnit);
698
+ if (this.pointsProgram.uPaletteSize)
699
+ gl.uniform1i(this.pointsProgram.uPaletteSize, this.paletteSize);
700
+ if (this.pointsProgram.uPaletteWidth)
701
+ gl.uniform1i(this.pointsProgram.uPaletteWidth, this.paletteWidth);
702
+ gl.bindVertexArray(this.pointsVao);
703
+ gl.drawArrays(gl.POINTS, 0, ds.n);
704
+ gl.bindVertexArray(null);
705
+ if (this.selectionVertexCount > 0) {
706
+ this.drawSolidPoints(this.selectionVao, this.selectionVertexCount, this.interactionStyle.selectionColor, this.pointRadiusCss + 1, true);
707
+ }
708
+ if (this.hoverVertexCount > 0) {
709
+ this.drawSolidPoints(this.hoverVao, this.hoverVertexCount, this.interactionStyle.hoverColor, this.pointRadiusCss + 3, true);
710
+ if (this.selection.has(this.hoveredIndex)) {
711
+ this.drawSolidPoints(this.hoverVao, this.hoverVertexCount, this.interactionStyle.selectionColor, this.pointRadiusCss + 1, true);
712
+ }
713
+ else {
714
+ const hoverColor = this.interactionStyle.hoverFillColor ?? this.resolveLabelColor(this.hoveredIndex);
715
+ this.drawSolidPoints(this.hoverVao, this.hoverVertexCount, hoverColor, this.pointRadiusCss + 1, false);
716
+ }
717
+ }
718
+ }
719
+ destroy() {
720
+ const gl = this.gl;
721
+ if (gl) {
722
+ if (this.pointsProgram)
723
+ gl.deleteProgram(this.pointsProgram.program);
724
+ if (this.solidProgram)
725
+ gl.deleteProgram(this.solidProgram.program);
726
+ if (this.guideProgram)
727
+ gl.deleteProgram(this.guideProgram.program);
728
+ if (this.pointsVao)
729
+ gl.deleteVertexArray(this.pointsVao);
730
+ if (this.selectionVao)
731
+ gl.deleteVertexArray(this.selectionVao);
732
+ if (this.hoverVao)
733
+ gl.deleteVertexArray(this.hoverVao);
734
+ if (this.guideVao)
735
+ gl.deleteVertexArray(this.guideVao);
736
+ if (this.pointsPosBuffer)
737
+ gl.deleteBuffer(this.pointsPosBuffer);
738
+ if (this.pointsLabelBuffer)
739
+ gl.deleteBuffer(this.pointsLabelBuffer);
740
+ if (this.selectionPosBuffer)
741
+ gl.deleteBuffer(this.selectionPosBuffer);
742
+ if (this.hoverPosBuffer)
743
+ gl.deleteBuffer(this.hoverPosBuffer);
744
+ if (this.guideBuffer)
745
+ gl.deleteBuffer(this.guideBuffer);
746
+ if (this.paletteTex)
747
+ gl.deleteTexture(this.paletteTex);
748
+ }
749
+ this.gl = null;
750
+ this.pointsProgram = null;
751
+ this.solidProgram = null;
752
+ this.guideProgram = null;
753
+ this.pointsVao = null;
754
+ this.selectionVao = null;
755
+ this.hoverVao = null;
756
+ this.guideVao = null;
757
+ this.pointsPosBuffer = null;
758
+ this.pointsLabelBuffer = null;
759
+ this.selectionPosBuffer = null;
760
+ this.hoverPosBuffer = null;
761
+ this.guideBuffer = null;
762
+ this.paletteTex = null;
763
+ }
764
+ ensureGL() {
765
+ if (this.gl)
766
+ return;
767
+ if (!this.canvas)
768
+ throw new Error("Renderer not initialized");
769
+ const gl = this.canvas.getContext("webgl2", {
770
+ antialias: false,
771
+ alpha: false,
772
+ depth: true,
773
+ stencil: false,
774
+ preserveDrawingBuffer: false,
775
+ premultipliedAlpha: false,
776
+ desynchronized: true,
777
+ });
778
+ if (!gl) {
779
+ throw new Error("Failed to get WebGL2 context");
780
+ }
781
+ this.gl = gl;
782
+ gl.viewport(0, 0, this.canvas.width, this.canvas.height);
783
+ this.createProgramsAndBuffers();
784
+ this.uploadPalette();
785
+ if (this.dataset) {
786
+ this.uploadDatasetToGPU();
787
+ this.rebuildGuideGeometry();
788
+ this.uploadSelectionToGPU();
789
+ this.uploadHoverToGPU();
790
+ }
791
+ this.projectedDirty = true;
792
+ }
793
+ createProgramsAndBuffers() {
794
+ const gl = this.gl;
795
+ if (!gl)
796
+ return;
797
+ const pointsProgram = linkProgram(gl, VS_POINTS_3D, FS_POINTS_3D);
798
+ const solidProgram = linkProgram(gl, VS_SOLID_3D, FS_SOLID_3D);
799
+ const guideProgram = linkProgram(gl, VS_GUIDE_3D, FS_GUIDE_3D);
800
+ this.pointsProgram = {
801
+ program: pointsProgram,
802
+ uMvp: gl.getUniformLocation(pointsProgram, "u_mvp"),
803
+ uPointSizePx: gl.getUniformLocation(pointsProgram, "u_pointSizePx"),
804
+ uPaletteTex: gl.getUniformLocation(pointsProgram, "u_paletteTex"),
805
+ uPaletteSize: gl.getUniformLocation(pointsProgram, "u_paletteSize"),
806
+ uPaletteWidth: gl.getUniformLocation(pointsProgram, "u_paletteWidth"),
807
+ };
808
+ this.solidProgram = {
809
+ program: solidProgram,
810
+ uMvp: gl.getUniformLocation(solidProgram, "u_mvp"),
811
+ uPointSizePx: gl.getUniformLocation(solidProgram, "u_pointSizePx"),
812
+ uColor: gl.getUniformLocation(solidProgram, "u_color"),
813
+ uRingMode: gl.getUniformLocation(solidProgram, "u_ringMode"),
814
+ uRingThicknessPx: gl.getUniformLocation(solidProgram, "u_ringThicknessPx"),
815
+ };
816
+ this.guideProgram = {
817
+ program: guideProgram,
818
+ uMvp: gl.getUniformLocation(guideProgram, "u_mvp"),
819
+ uColor: gl.getUniformLocation(guideProgram, "u_color"),
820
+ };
821
+ this.pointsVao = gl.createVertexArray();
822
+ this.pointsPosBuffer = gl.createBuffer();
823
+ this.pointsLabelBuffer = gl.createBuffer();
824
+ this.selectionVao = gl.createVertexArray();
825
+ this.selectionPosBuffer = gl.createBuffer();
826
+ this.hoverVao = gl.createVertexArray();
827
+ this.hoverPosBuffer = gl.createBuffer();
828
+ this.guideVao = gl.createVertexArray();
829
+ this.guideBuffer = gl.createBuffer();
830
+ if (!this.pointsVao ||
831
+ !this.pointsPosBuffer ||
832
+ !this.pointsLabelBuffer ||
833
+ !this.selectionVao ||
834
+ !this.selectionPosBuffer ||
835
+ !this.hoverVao ||
836
+ !this.hoverPosBuffer ||
837
+ !this.guideVao ||
838
+ !this.guideBuffer) {
839
+ throw new Error("Failed to allocate WebGL buffers for 3D renderer");
840
+ }
841
+ gl.bindVertexArray(this.pointsVao);
842
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.pointsPosBuffer);
843
+ gl.enableVertexAttribArray(0);
844
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
845
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.pointsLabelBuffer);
846
+ gl.enableVertexAttribArray(1);
847
+ gl.vertexAttribIPointer(1, 1, gl.UNSIGNED_SHORT, 0, 0);
848
+ gl.bindVertexArray(null);
849
+ gl.bindVertexArray(this.selectionVao);
850
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.selectionPosBuffer);
851
+ gl.enableVertexAttribArray(0);
852
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
853
+ gl.bindVertexArray(null);
854
+ gl.bindVertexArray(this.hoverVao);
855
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.hoverPosBuffer);
856
+ gl.enableVertexAttribArray(0);
857
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
858
+ gl.bindVertexArray(null);
859
+ gl.bindVertexArray(this.guideVao);
860
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.guideBuffer);
861
+ gl.enableVertexAttribArray(0);
862
+ gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0);
863
+ gl.bindVertexArray(null);
864
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
865
+ }
866
+ uploadPalette() {
867
+ const gl = this.gl;
868
+ if (!gl || !this.pointsProgram)
869
+ return;
870
+ const rawSize = this.colors.length;
871
+ const size = Math.max(1, Math.min(rawSize, 0xffff + 1));
872
+ const maxTex = gl.getParameter(gl.MAX_TEXTURE_SIZE);
873
+ const texW = Math.min(maxTex, size);
874
+ const texH = Math.ceil(size / texW);
875
+ const capacity = texW * texH;
876
+ if (this.paletteBytes.length !== capacity * 4) {
877
+ this.paletteBytes = new Uint8Array(capacity * 4);
878
+ }
879
+ else {
880
+ this.paletteBytes.fill(0);
881
+ }
882
+ if (rawSize === 0) {
883
+ this.paletteBytes[0] = 255;
884
+ this.paletteBytes[1] = 255;
885
+ this.paletteBytes[2] = 255;
886
+ const visible = this.isCategoryVisible(0);
887
+ this.paletteBytes[3] = visible ? Math.round(255 * this.categoryAlpha) : 0;
888
+ }
889
+ else {
890
+ for (let i = 0; i < size; i++) {
891
+ const [r, g, b, a] = parseHexColorBytes(this.colors[i]);
892
+ const visible = this.isCategoryVisible(i);
893
+ const alpha = visible ? Math.round(a * this.categoryAlpha) : 0;
894
+ const o = i * 4;
895
+ this.paletteBytes[o] = r;
896
+ this.paletteBytes[o + 1] = g;
897
+ this.paletteBytes[o + 2] = b;
898
+ this.paletteBytes[o + 3] = alpha;
899
+ }
900
+ }
901
+ if (!this.paletteTex) {
902
+ this.paletteTex = gl.createTexture();
903
+ if (!this.paletteTex)
904
+ throw new Error("Failed to create palette texture");
905
+ gl.activeTexture(gl.TEXTURE0 + this.paletteTexUnit);
906
+ gl.bindTexture(gl.TEXTURE_2D, this.paletteTex);
907
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
908
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
909
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
910
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
911
+ gl.bindTexture(gl.TEXTURE_2D, null);
912
+ gl.activeTexture(gl.TEXTURE0);
913
+ }
914
+ this.paletteSize = size;
915
+ this.paletteWidth = texW;
916
+ this.paletteHeight = texH;
917
+ gl.activeTexture(gl.TEXTURE0 + this.paletteTexUnit);
918
+ gl.bindTexture(gl.TEXTURE_2D, this.paletteTex);
919
+ gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);
920
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, texW, texH, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.paletteBytes);
921
+ gl.bindTexture(gl.TEXTURE_2D, null);
922
+ gl.activeTexture(gl.TEXTURE0);
923
+ }
924
+ bindPaletteTexture() {
925
+ const gl = this.gl;
926
+ if (!gl || !this.paletteTex)
927
+ return;
928
+ gl.activeTexture(gl.TEXTURE0 + this.paletteTexUnit);
929
+ gl.bindTexture(gl.TEXTURE_2D, this.paletteTex);
930
+ gl.activeTexture(gl.TEXTURE0);
931
+ }
932
+ uploadDatasetToGPU() {
933
+ const gl = this.gl;
934
+ const ds = this.dataset;
935
+ if (!gl || !ds || !this.pointsVao || !this.pointsPosBuffer || !this.pointsLabelBuffer)
936
+ return;
937
+ gl.bindVertexArray(this.pointsVao);
938
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.pointsPosBuffer);
939
+ gl.bufferData(gl.ARRAY_BUFFER, ds.positions, gl.STATIC_DRAW);
940
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.pointsLabelBuffer);
941
+ gl.bufferData(gl.ARRAY_BUFFER, ds.labels, gl.STATIC_DRAW);
942
+ gl.bindVertexArray(null);
943
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
944
+ }
945
+ drawSolidPoints(vao, count, color, radiusCss, ring) {
946
+ const gl = this.gl;
947
+ const program = this.solidProgram;
948
+ if (!gl || !program || !vao || count <= 0)
949
+ return;
950
+ const [r, g, b, a] = parseHexColor(color);
951
+ gl.useProgram(program.program);
952
+ if (program.uMvp)
953
+ gl.uniformMatrix4fv(program.uMvp, false, this.mvpMatrix);
954
+ if (program.uPointSizePx)
955
+ gl.uniform1f(program.uPointSizePx, radiusCss * 2 * this.dpr);
956
+ if (program.uColor)
957
+ gl.uniform4f(program.uColor, r, g, b, a);
958
+ if (program.uRingMode)
959
+ gl.uniform1i(program.uRingMode, ring ? 1 : 0);
960
+ if (program.uRingThicknessPx)
961
+ gl.uniform1f(program.uRingThicknessPx, ring ? 2 : 0);
962
+ gl.bindVertexArray(vao);
963
+ gl.drawArrays(gl.POINTS, 0, count);
964
+ gl.bindVertexArray(null);
965
+ }
966
+ drawSphereGuide() {
967
+ const gl = this.gl;
968
+ const program = this.guideProgram;
969
+ if (!gl || !program || !this.guideVao || this.guideVertexCount === 0)
970
+ return;
971
+ const [r, g, b] = parseHexColor(this.sphereGuideColor);
972
+ gl.disable(gl.DEPTH_TEST);
973
+ gl.enable(gl.BLEND);
974
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
975
+ gl.useProgram(program.program);
976
+ if (program.uMvp)
977
+ gl.uniformMatrix4fv(program.uMvp, false, this.mvpMatrix);
978
+ if (program.uColor)
979
+ gl.uniform4f(program.uColor, r, g, b, this.sphereGuideOpacity);
980
+ gl.bindVertexArray(this.guideVao);
981
+ const seg = this.guideSegmentVerts;
982
+ gl.drawArrays(gl.LINE_STRIP, 0, seg);
983
+ gl.drawArrays(gl.LINE_STRIP, seg, seg);
984
+ gl.drawArrays(gl.LINE_STRIP, seg * 2, seg);
985
+ gl.bindVertexArray(null);
986
+ gl.enable(gl.DEPTH_TEST);
987
+ }
988
+ drawEuclideanGuide() {
989
+ const gl = this.gl;
990
+ const program = this.guideProgram;
991
+ if (!gl || !program || !this.guideVao || this.guideAxisVertexCount === 0)
992
+ return;
993
+ const [r, g, b] = parseHexColor(this.sphereGuideColor);
994
+ const axisOpacity = clamp(this.sphereGuideOpacity * 0.8, 0.06, 0.18);
995
+ gl.enable(gl.DEPTH_TEST);
996
+ gl.depthMask(false);
997
+ gl.enable(gl.BLEND);
998
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
999
+ gl.useProgram(program.program);
1000
+ if (program.uMvp)
1001
+ gl.uniformMatrix4fv(program.uMvp, false, this.mvpMatrix);
1002
+ if (program.uColor)
1003
+ gl.uniform4f(program.uColor, r, g, b, axisOpacity);
1004
+ gl.bindVertexArray(this.guideVao);
1005
+ gl.drawArrays(gl.LINES, this.guideAxisVertexOffset, this.guideAxisVertexCount);
1006
+ gl.bindVertexArray(null);
1007
+ gl.depthMask(true);
1008
+ }
1009
+ uploadSelectionToGPU() {
1010
+ const gl = this.gl;
1011
+ const ds = this.dataset;
1012
+ if (!gl || !ds || !this.selectionPosBuffer)
1013
+ return;
1014
+ if (this.selection.size === 0) {
1015
+ this.selectionVertexCount = 0;
1016
+ this.selectionDirty = false;
1017
+ return;
1018
+ }
1019
+ const renderCount = Math.min(this.selection.size, MAX_SELECTION_RENDER_POINTS);
1020
+ if (this.selectionPositionsScratch.length < renderCount * 3) {
1021
+ this.selectionPositionsScratch = new Float32Array(renderCount * 3);
1022
+ }
1023
+ let k = 0;
1024
+ for (const i of this.selection) {
1025
+ if (i < 0 || i >= ds.n)
1026
+ continue;
1027
+ if (!this.isCategoryVisible(ds.labels[i]))
1028
+ continue;
1029
+ this.selectionPositionsScratch[k * 3] = ds.positions[i * 3];
1030
+ this.selectionPositionsScratch[k * 3 + 1] = ds.positions[i * 3 + 1];
1031
+ this.selectionPositionsScratch[k * 3 + 2] = ds.positions[i * 3 + 2];
1032
+ k++;
1033
+ if (k >= renderCount)
1034
+ break;
1035
+ }
1036
+ this.selectionVertexCount = k;
1037
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.selectionPosBuffer);
1038
+ gl.bufferData(gl.ARRAY_BUFFER, this.selectionPositionsScratch.subarray(0, k * 3), gl.DYNAMIC_DRAW);
1039
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
1040
+ this.selectionDirty = false;
1041
+ }
1042
+ uploadHoverToGPU() {
1043
+ const gl = this.gl;
1044
+ const ds = this.dataset;
1045
+ if (!gl || !ds || !this.hoverPosBuffer)
1046
+ return;
1047
+ const i = this.hoveredIndex;
1048
+ if (i < 0 || i >= ds.n || !this.isCategoryVisible(ds.labels[i])) {
1049
+ this.hoverVertexCount = 0;
1050
+ this.hoverDirty = false;
1051
+ return;
1052
+ }
1053
+ this.hoverPositionScratch[0] = ds.positions[i * 3];
1054
+ this.hoverPositionScratch[1] = ds.positions[i * 3 + 1];
1055
+ this.hoverPositionScratch[2] = ds.positions[i * 3 + 2];
1056
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.hoverPosBuffer);
1057
+ gl.bufferData(gl.ARRAY_BUFFER, this.hoverPositionScratch, gl.DYNAMIC_DRAW);
1058
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
1059
+ this.hoverVertexCount = 1;
1060
+ this.hoverDirty = false;
1061
+ }
1062
+ fitViewToDataset() {
1063
+ const ds = this.dataset;
1064
+ if (!ds || ds.n === 0)
1065
+ return;
1066
+ let centerX = 0;
1067
+ let centerY = 0;
1068
+ let centerZ = 0;
1069
+ for (let i = 0; i < ds.n; i++) {
1070
+ centerX += ds.positions[i * 3];
1071
+ centerY += ds.positions[i * 3 + 1];
1072
+ centerZ += ds.positions[i * 3 + 2];
1073
+ }
1074
+ centerX /= ds.n;
1075
+ centerY /= ds.n;
1076
+ centerZ /= ds.n;
1077
+ let radius = 0;
1078
+ for (let i = 0; i < ds.n; i++) {
1079
+ const dx = ds.positions[i * 3] - centerX;
1080
+ const dy = ds.positions[i * 3 + 1] - centerY;
1081
+ const dz = ds.positions[i * 3 + 2] - centerZ;
1082
+ radius = Math.max(radius, Math.hypot(dx, dy, dz));
1083
+ }
1084
+ this.sceneRadius = Math.max(radius, 0.25);
1085
+ this.view = {
1086
+ type: "orbit3d",
1087
+ yaw: 0.7,
1088
+ pitch: 0.35,
1089
+ distance: Math.max(this.sceneRadius * 3.0, 1.5),
1090
+ targetX: centerX,
1091
+ targetY: centerY,
1092
+ targetZ: centerZ,
1093
+ orthoScale: Math.max(this.sceneRadius * 1.4, 0.75),
1094
+ };
1095
+ this.projectedDirty = true;
1096
+ }
1097
+ getCameraFrame() {
1098
+ const cp = Math.cos(this.view.pitch);
1099
+ const sp = Math.sin(this.view.pitch);
1100
+ const cy = Math.cos(this.view.yaw);
1101
+ const sy = Math.sin(this.view.yaw);
1102
+ const dirTargetToEye = [cp * sy, sp, cp * cy];
1103
+ const target = [this.view.targetX, this.view.targetY, this.view.targetZ];
1104
+ const eye = vec3Add(target, vec3Scale(dirTargetToEye, this.view.distance));
1105
+ let forward = vec3Normalize(vec3Sub(target, eye));
1106
+ if (vec3Length(forward) < 1e-9) {
1107
+ forward = [0, 0, -1];
1108
+ }
1109
+ let right = vec3Normalize(vec3Cross(forward, WORLD_UP));
1110
+ if (vec3Length(right) < 1e-9) {
1111
+ right = [1, 0, 0];
1112
+ }
1113
+ const up = vec3Normalize(vec3Cross(right, forward));
1114
+ return { eye, target, right, up, forward };
1115
+ }
1116
+ updateMvpMatrix() {
1117
+ const { eye, target, up } = this.getCameraFrame();
1118
+ const aspect = this.width > 0 && this.height > 0 ? this.width / this.height : 1;
1119
+ const halfH = Math.max(0.01, this.view.orthoScale);
1120
+ const halfW = halfH * aspect;
1121
+ const near = 0.01;
1122
+ const far = Math.max(near + 1, this.view.distance + this.sceneRadius * 6 + 10);
1123
+ const view = mat4LookAt(eye, target, up);
1124
+ const proj = mat4Ortho(-halfW, halfW, -halfH, halfH, near, far);
1125
+ this.mvpMatrix = mat4Multiply(proj, view);
1126
+ }
1127
+ ensureProjectedCapacity(n) {
1128
+ if (this.projectedScreenX.length !== n)
1129
+ this.projectedScreenX = new Float32Array(n);
1130
+ if (this.projectedScreenY.length !== n)
1131
+ this.projectedScreenY = new Float32Array(n);
1132
+ if (this.projectedDepth.length !== n)
1133
+ this.projectedDepth = new Float32Array(n);
1134
+ if (this.projectedPixelIndex.length !== n)
1135
+ this.projectedPixelIndex = new Int32Array(n);
1136
+ if (this.projectedVisible.length !== n)
1137
+ this.projectedVisible = new Uint8Array(n);
1138
+ if (this.projectedVisibleIndices.length !== n)
1139
+ this.projectedVisibleIndices = new Uint32Array(n);
1140
+ const pixelCount = Math.max(1, this.width * this.height);
1141
+ if (this.depthBuffer.length !== pixelCount) {
1142
+ this.depthBuffer = new Float32Array(pixelCount);
1143
+ }
1144
+ }
1145
+ ensureProjectedCache() {
1146
+ if (!this.projectedDirty)
1147
+ return;
1148
+ const ds = this.dataset;
1149
+ if (!ds)
1150
+ return;
1151
+ this.updateMvpMatrix();
1152
+ this.ensureProjectedCapacity(ds.n);
1153
+ this.depthBuffer.fill(Number.POSITIVE_INFINITY);
1154
+ this.projectedVisibleCount = 0;
1155
+ const w = Math.max(1, this.width);
1156
+ const h = Math.max(1, this.height);
1157
+ const useVisibilityMask = this.hasCategoryVisibilityMask;
1158
+ const visibilityMask = this.categoryVisibilityMask;
1159
+ for (let i = 0; i < ds.n; i++) {
1160
+ if (useVisibilityMask) {
1161
+ const label = ds.labels[i];
1162
+ if (label < visibilityMask.length && visibilityMask[label] === 0) {
1163
+ this.projectedPixelIndex[i] = -1;
1164
+ this.projectedVisible[i] = 0;
1165
+ continue;
1166
+ }
1167
+ }
1168
+ const x = ds.positions[i * 3];
1169
+ const y = ds.positions[i * 3 + 1];
1170
+ const z = ds.positions[i * 3 + 2];
1171
+ const [clipX, clipY, clipZ, clipW] = transformClip(this.mvpMatrix, x, y, z);
1172
+ const invW = Math.abs(clipW) > 1e-12 ? 1 / clipW : 0;
1173
+ const ndcX = clipX * invW;
1174
+ const ndcY = clipY * invW;
1175
+ const ndcZ = clipZ * invW;
1176
+ if (ndcX < -1 || ndcX > 1 || ndcY < -1 || ndcY > 1 || ndcZ < -1 || ndcZ > 1) {
1177
+ this.projectedPixelIndex[i] = -1;
1178
+ this.projectedVisible[i] = 0;
1179
+ continue;
1180
+ }
1181
+ const sx = (ndcX * 0.5 + 0.5) * w;
1182
+ const sy = (1 - (ndcY * 0.5 + 0.5)) * h;
1183
+ const depth = ndcZ * 0.5 + 0.5;
1184
+ const ix = Math.floor(sx);
1185
+ const iy = Math.floor(sy);
1186
+ if (ix < 0 || ix >= w || iy < 0 || iy >= h) {
1187
+ this.projectedPixelIndex[i] = -1;
1188
+ this.projectedVisible[i] = 0;
1189
+ continue;
1190
+ }
1191
+ const p = iy * w + ix;
1192
+ this.projectedScreenX[i] = sx;
1193
+ this.projectedScreenY[i] = sy;
1194
+ this.projectedDepth[i] = depth;
1195
+ this.projectedPixelIndex[i] = p;
1196
+ this.projectedVisible[i] = 0;
1197
+ if (depth < this.depthBuffer[p]) {
1198
+ this.depthBuffer[p] = depth;
1199
+ }
1200
+ }
1201
+ for (let i = 0; i < ds.n; i++) {
1202
+ const p = this.projectedPixelIndex[i];
1203
+ if (p < 0)
1204
+ continue;
1205
+ const depth = this.projectedDepth[i];
1206
+ if (depth <= this.depthBuffer[p] + 1e-4) {
1207
+ this.projectedVisible[i] = 1;
1208
+ this.projectedVisibleIndices[this.projectedVisibleCount++] = i;
1209
+ }
1210
+ }
1211
+ this.projectedDirty = false;
1212
+ }
1213
+ rebuildGuideGeometry() {
1214
+ const gl = this.gl;
1215
+ if (!gl || !this.guideBuffer)
1216
+ return;
1217
+ this.guideVertexCount = 0;
1218
+ this.guideSegmentVerts = 0;
1219
+ this.guideAxisVertexOffset = 0;
1220
+ this.guideAxisVertexCount = 0;
1221
+ if (this.supportsSphereGuide()) {
1222
+ const radius = this.estimateGuideRadius();
1223
+ const segments = 128;
1224
+ const vertsPerCircle = segments + 1;
1225
+ const out = new Float32Array(vertsPerCircle * 3 * 3);
1226
+ let k = 0;
1227
+ for (let i = 0; i <= segments; i++) {
1228
+ const a = (i / segments) * Math.PI * 2;
1229
+ out[k++] = radius * Math.cos(a);
1230
+ out[k++] = radius * Math.sin(a);
1231
+ out[k++] = 0;
1232
+ }
1233
+ for (let i = 0; i <= segments; i++) {
1234
+ const a = (i / segments) * Math.PI * 2;
1235
+ out[k++] = radius * Math.cos(a);
1236
+ out[k++] = 0;
1237
+ out[k++] = radius * Math.sin(a);
1238
+ }
1239
+ for (let i = 0; i <= segments; i++) {
1240
+ const a = (i / segments) * Math.PI * 2;
1241
+ out[k++] = 0;
1242
+ out[k++] = radius * Math.cos(a);
1243
+ out[k++] = radius * Math.sin(a);
1244
+ }
1245
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.guideBuffer);
1246
+ gl.bufferData(gl.ARRAY_BUFFER, out, gl.STATIC_DRAW);
1247
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
1248
+ this.guideVertexCount = out.length / 3;
1249
+ this.guideSegmentVerts = vertsPerCircle;
1250
+ return;
1251
+ }
1252
+ if (!this.supportsEuclideanGuide())
1253
+ return;
1254
+ const ds = this.dataset;
1255
+ if (!ds || ds.n === 0)
1256
+ return;
1257
+ let minX = Number.POSITIVE_INFINITY;
1258
+ let minY = Number.POSITIVE_INFINITY;
1259
+ let minZ = Number.POSITIVE_INFINITY;
1260
+ let maxX = Number.NEGATIVE_INFINITY;
1261
+ let maxY = Number.NEGATIVE_INFINITY;
1262
+ let maxZ = Number.NEGATIVE_INFINITY;
1263
+ for (let i = 0; i < ds.n; i++) {
1264
+ const x = ds.positions[i * 3];
1265
+ const y = ds.positions[i * 3 + 1];
1266
+ const z = ds.positions[i * 3 + 2];
1267
+ if (x < minX)
1268
+ minX = x;
1269
+ if (y < minY)
1270
+ minY = y;
1271
+ if (z < minZ)
1272
+ minZ = z;
1273
+ if (x > maxX)
1274
+ maxX = x;
1275
+ if (y > maxY)
1276
+ maxY = y;
1277
+ if (z > maxZ)
1278
+ maxZ = z;
1279
+ }
1280
+ const spanX = Math.max(0, maxX - minX);
1281
+ const spanY = Math.max(0, maxY - minY);
1282
+ const spanZ = Math.max(0, maxZ - minZ);
1283
+ const longestSpan = Math.max(spanX, spanY, spanZ, this.sceneRadius * 2, 1e-3);
1284
+ const pad = longestSpan * 0.04;
1285
+ const fallbackHalf = Math.max(longestSpan * 0.3, 0.2);
1286
+ const centerX = (minX + maxX) * 0.5;
1287
+ const centerY = (minY + maxY) * 0.5;
1288
+ const centerZ = (minZ + maxZ) * 0.5;
1289
+ const halfX = spanX > 1e-4 ? spanX * 0.5 + pad : fallbackHalf;
1290
+ const halfY = spanY > 1e-4 ? spanY * 0.5 + pad : fallbackHalf;
1291
+ const halfZ = spanZ > 1e-4 ? spanZ * 0.5 + pad : fallbackHalf;
1292
+ const out = new Float32Array(6 * 3);
1293
+ let k = 0;
1294
+ out[k++] = centerX - halfX;
1295
+ out[k++] = centerY;
1296
+ out[k++] = centerZ;
1297
+ out[k++] = centerX + halfX;
1298
+ out[k++] = centerY;
1299
+ out[k++] = centerZ;
1300
+ out[k++] = centerX;
1301
+ out[k++] = centerY - halfY;
1302
+ out[k++] = centerZ;
1303
+ out[k++] = centerX;
1304
+ out[k++] = centerY + halfY;
1305
+ out[k++] = centerZ;
1306
+ out[k++] = centerX;
1307
+ out[k++] = centerY;
1308
+ out[k++] = centerZ - halfZ;
1309
+ out[k++] = centerX;
1310
+ out[k++] = centerY;
1311
+ out[k++] = centerZ + halfZ;
1312
+ gl.bindBuffer(gl.ARRAY_BUFFER, this.guideBuffer);
1313
+ gl.bufferData(gl.ARRAY_BUFFER, out, gl.STATIC_DRAW);
1314
+ gl.bindBuffer(gl.ARRAY_BUFFER, null);
1315
+ this.guideVertexCount = out.length / 3;
1316
+ this.guideAxisVertexOffset = 0;
1317
+ this.guideAxisVertexCount = 6;
1318
+ }
1319
+ estimateGuideRadius() {
1320
+ const ds = this.dataset;
1321
+ if (!ds || ds.n === 0)
1322
+ return 1;
1323
+ let maxNorm = 0;
1324
+ for (let i = 0; i < ds.n; i++) {
1325
+ const x = ds.positions[i * 3];
1326
+ const y = ds.positions[i * 3 + 1];
1327
+ const z = ds.positions[i * 3 + 2];
1328
+ maxNorm = Math.max(maxNorm, Math.hypot(x, y, z));
1329
+ }
1330
+ return Math.max(0.25, maxNorm);
1331
+ }
1332
+ resolveLabelColor(index) {
1333
+ const ds = this.dataset;
1334
+ if (!ds || index < 0 || index >= ds.n || this.colors.length === 0) {
1335
+ return HOVER_COLOR;
1336
+ }
1337
+ const label = ds.labels[index];
1338
+ return this.colors[label % this.colors.length];
1339
+ }
1340
+ }
1341
+ export class Euclidean3DWebGLCandidate extends PointCloud3DWebGLBase {
1342
+ expectedGeometry() {
1343
+ return "euclidean3d";
1344
+ }
1345
+ supportsEuclideanGuide() {
1346
+ return true;
1347
+ }
1348
+ }
1349
+ export class Spherical3DWebGLCandidate extends PointCloud3DWebGLBase {
1350
+ expectedGeometry() {
1351
+ return "sphere";
1352
+ }
1353
+ supportsSphereGuide() {
1354
+ return true;
1355
+ }
1356
+ preprocessPositions(input) {
1357
+ const out = new Float32Array(input.length);
1358
+ for (let i = 0; i < input.length; i += 3) {
1359
+ const x = input[i];
1360
+ const y = input[i + 1];
1361
+ const z = input[i + 2];
1362
+ const norm = Math.hypot(x, y, z);
1363
+ if (norm < 1e-8) {
1364
+ out[i] = 0;
1365
+ out[i + 1] = 0;
1366
+ out[i + 2] = 1;
1367
+ }
1368
+ else {
1369
+ out[i] = x / norm;
1370
+ out[i + 1] = y / norm;
1371
+ out[i + 2] = z / norm;
1372
+ }
1373
+ }
1374
+ return out;
1375
+ }
1376
+ fitViewToDataset() {
1377
+ const ds = this.dataset;
1378
+ if (!ds || ds.n === 0)
1379
+ return;
1380
+ let radius = 0;
1381
+ for (let i = 0; i < ds.n; i++) {
1382
+ const x = ds.positions[i * 3];
1383
+ const y = ds.positions[i * 3 + 1];
1384
+ const z = ds.positions[i * 3 + 2];
1385
+ radius = Math.max(radius, Math.hypot(x, y, z));
1386
+ }
1387
+ this.sceneRadius = Math.max(radius, 1);
1388
+ this.view = {
1389
+ type: "orbit3d",
1390
+ yaw: 0.9,
1391
+ pitch: 0.4,
1392
+ distance: Math.max(this.sceneRadius * 3.2, 2.4),
1393
+ targetX: 0,
1394
+ targetY: 0,
1395
+ targetZ: 0,
1396
+ orthoScale: Math.max(this.sceneRadius * 1.45, 1.0),
1397
+ };
1398
+ this.projectedDirty = true;
1399
+ }
1400
+ estimateGuideRadius() {
1401
+ return 1;
1402
+ }
1403
+ }
1404
+ //# sourceMappingURL=webgl_candidate_3d.js.map