three-cad-viewer 4.3.4 → 4.3.6

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.
Files changed (59) hide show
  1. package/dist/scene/clipping.d.ts +6 -0
  2. package/dist/three-cad-viewer.esm.js +20 -5
  3. package/dist/three-cad-viewer.esm.js.map +1 -1
  4. package/dist/three-cad-viewer.esm.min.js +1 -1
  5. package/dist/three-cad-viewer.js +20 -5
  6. package/dist/three-cad-viewer.min.js +1 -1
  7. package/package.json +2 -3
  8. package/src/_version.ts +0 -1
  9. package/src/camera/camera.ts +0 -445
  10. package/src/camera/controls/CADOrbitControls.ts +0 -241
  11. package/src/camera/controls/CADTrackballControls.ts +0 -598
  12. package/src/camera/controls.ts +0 -380
  13. package/src/core/patches.ts +0 -16
  14. package/src/core/studio-manager.ts +0 -652
  15. package/src/core/types.ts +0 -892
  16. package/src/core/viewer-state.ts +0 -784
  17. package/src/core/viewer.ts +0 -4821
  18. package/src/index.ts +0 -151
  19. package/src/rendering/environment.ts +0 -840
  20. package/src/rendering/light-detection.ts +0 -327
  21. package/src/rendering/material-factory.ts +0 -735
  22. package/src/rendering/material-presets.ts +0 -289
  23. package/src/rendering/raycast.ts +0 -291
  24. package/src/rendering/room-environment.ts +0 -192
  25. package/src/rendering/studio-composer.ts +0 -577
  26. package/src/rendering/studio-floor.ts +0 -108
  27. package/src/rendering/texture-cache.ts +0 -324
  28. package/src/rendering/tree-model.ts +0 -542
  29. package/src/rendering/triplanar.ts +0 -329
  30. package/src/scene/animation.ts +0 -343
  31. package/src/scene/axes.ts +0 -108
  32. package/src/scene/bbox.ts +0 -223
  33. package/src/scene/clipping.ts +0 -640
  34. package/src/scene/grid.ts +0 -864
  35. package/src/scene/nestedgroup.ts +0 -1444
  36. package/src/scene/objectgroup.ts +0 -866
  37. package/src/scene/orientation.ts +0 -259
  38. package/src/scene/render-shape.ts +0 -634
  39. package/src/tools/cad_tools/measure.ts +0 -811
  40. package/src/tools/cad_tools/select.ts +0 -100
  41. package/src/tools/cad_tools/tools.ts +0 -231
  42. package/src/tools/cad_tools/ui.ts +0 -454
  43. package/src/tools/cad_tools/zebra.ts +0 -369
  44. package/src/types/html.d.ts +0 -5
  45. package/src/types/n8ao.d.ts +0 -28
  46. package/src/types/three-augmentation.d.ts +0 -60
  47. package/src/ui/display.ts +0 -3295
  48. package/src/ui/index.html +0 -505
  49. package/src/ui/info.ts +0 -177
  50. package/src/ui/slider.ts +0 -206
  51. package/src/ui/toolbar.ts +0 -347
  52. package/src/ui/treeview.ts +0 -945
  53. package/src/utils/decode-instances.ts +0 -233
  54. package/src/utils/font.ts +0 -60
  55. package/src/utils/gpu-tracker.ts +0 -265
  56. package/src/utils/logger.ts +0 -92
  57. package/src/utils/sizeof.ts +0 -116
  58. package/src/utils/timer.ts +0 -69
  59. package/src/utils/utils.ts +0 -446
package/src/scene/grid.ts DELETED
@@ -1,864 +0,0 @@
1
- import * as THREE from "three";
2
- import { deepDispose } from "../utils/utils.js";
3
- import { CompoundGroup } from "./nestedgroup.js";
4
- import type { Theme } from "../core/types";
5
- import type { BoundingBox } from "./bbox.js";
6
-
7
- /**
8
- * Linear interpolation with capping at boundaries
9
- * @param px1 - X coordinate of first point
10
- * @param py1 - Y coordinate of first point (output when x <= px1)
11
- * @param px2 - X coordinate of second point
12
- * @param py2 - Y coordinate of second point (output when x >= px2)
13
- * @param x - Input value to interpolate
14
- * @returns Interpolated value, capped at py1 or py2 if outside range
15
- */
16
- function cappedLinear(px1: number, py1: number, px2: number, py2: number, x: number): number {
17
- const m = (py2 - py1) / (px2 - px1);
18
- return x < px1 ? py1 : x > px2 ? py2 : m * (x - px1) + py1;
19
- }
20
-
21
- /**
22
- * Format a number string by removing trailing zeros while keeping at least one decimal
23
- * @param str - Number string to format
24
- * @returns Formatted string (e.g., "1.500" -> "1.5", "2.000" -> "2.0", "-0" -> "0")
25
- */
26
- function trimTrailingZeros(str: string): string {
27
- let result = str
28
- .replace(/(\.\d*[1-9])0+$/, "$1") // Remove zeros after nonzero decimals
29
- .replace(/\.0+$/, ""); // Remove .000... case
30
- if (result === "-0") result = "0"; // Handle negative zero case
31
- if (result.indexOf(".") < 0) result = `${result}.0`; // Ensure at least one decimal place
32
- return result;
33
- }
34
-
35
- /**
36
- * Creates a grid plane with dashed grid lines and solid colored centerlines.
37
- * Used internally by Grid to create XY, XZ, and YZ plane grids.
38
- */
39
- class GridHelper extends THREE.Object3D {
40
- /**
41
- * Create a GridHelper
42
- * @param size - Total size of the grid (width and height)
43
- * @param divisions - Number of divisions (grid lines)
44
- * @param colorX - Color for the X-axis centerline
45
- * @param colorY - Color for the Y-axis centerline
46
- * @param colorGrid - Color for the dashed grid lines
47
- */
48
- constructor(
49
- size: number,
50
- divisions: number,
51
- colorX: number | string,
52
- colorY: number | string,
53
- colorGrid: number | string
54
- ) {
55
- super();
56
-
57
- const step = size / divisions;
58
- const halfSize = size / 2;
59
- const vertices: number[] = [];
60
- const gridColors: (number | string)[] = [];
61
- const solidVerticesX: number[] = [];
62
- const solidVerticesY: number[] = [];
63
-
64
- // Track whether centerlines have been added (should only happen once)
65
- let centerlineXAdded = false;
66
- let centerlineYAdded = false;
67
-
68
- // Create grid lines
69
- for (let i = 0; i <= divisions; i++) {
70
- const k = -halfSize + i * step;
71
- const isCenter = Math.abs(k) < 1e-10;
72
-
73
- // Vertical lines (parallel to Y axis)
74
- if (!isCenter) {
75
- // Dashed grid line
76
- vertices.push(-halfSize, 0, k, halfSize, 0, k);
77
- gridColors.push(colorGrid, colorGrid);
78
- } else if (!centerlineYAdded) {
79
- // Solid centerline Y (only add once)
80
- solidVerticesY.push(-halfSize, 0, 0, halfSize, 0, 0);
81
- centerlineYAdded = true;
82
- }
83
-
84
- // Horizontal lines (parallel to X axis)
85
- if (!isCenter) {
86
- // Dashed grid line
87
- vertices.push(k, 0, -halfSize, k, 0, halfSize);
88
- gridColors.push(colorGrid, colorGrid);
89
- } else if (!centerlineXAdded) {
90
- // Solid centerline X (only add once)
91
- solidVerticesX.push(0, 0, -halfSize, 0, 0, halfSize);
92
- centerlineXAdded = true;
93
- }
94
- }
95
-
96
- // Ensure centerlines exist even if grid doesn't pass through zero
97
- if (!centerlineYAdded) {
98
- solidVerticesY.push(-halfSize, 0, 0, halfSize, 0, 0);
99
- }
100
- if (!centerlineXAdded) {
101
- solidVerticesX.push(0, 0, -halfSize, 0, 0, halfSize);
102
- }
103
-
104
- // Dashed grid lines
105
- const dashedGeometry = new THREE.BufferGeometry();
106
- dashedGeometry.setAttribute(
107
- "position",
108
- new THREE.Float32BufferAttribute(vertices, 3),
109
- );
110
- // Compute line distances for dashed lines
111
- const position = dashedGeometry.getAttribute("position");
112
- const lineDistances = new Float32Array(position.count);
113
- for (let i = 0; i < position.count; i += 2) {
114
- const x1 = position.getX(i),
115
- y1 = position.getY(i),
116
- z1 = position.getZ(i);
117
- const x2 = position.getX(i + 1),
118
- y2 = position.getY(i + 1),
119
- z2 = position.getZ(i + 1);
120
- lineDistances[i] = 0;
121
- lineDistances[i + 1] = Math.sqrt(
122
- (x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2,
123
- );
124
- }
125
- dashedGeometry.setAttribute(
126
- "lineDistance",
127
- new THREE.BufferAttribute(lineDistances, 1),
128
- );
129
-
130
- const dashedMaterial = new THREE.LineDashedMaterial({
131
- color: colorGrid,
132
- dashSize: step / 20,
133
- gapSize: step / 20,
134
- opacity: 1,
135
- transparent: false,
136
- vertexColors: false,
137
- });
138
-
139
- const dashedLines = new THREE.LineSegments(dashedGeometry, dashedMaterial);
140
- this.add(dashedLines);
141
-
142
- // Centerline X (solid)
143
- const xGeometry = new THREE.BufferGeometry();
144
- xGeometry.setAttribute(
145
- "position",
146
- new THREE.Float32BufferAttribute(solidVerticesX, 3),
147
- );
148
- const xMaterial = new THREE.LineBasicMaterial({ color: colorX });
149
- this.add(new THREE.LineSegments(xGeometry, xMaterial));
150
-
151
- // Centerline Y (solid)
152
- const yGeometry = new THREE.BufferGeometry();
153
- yGeometry.setAttribute(
154
- "position",
155
- new THREE.Float32BufferAttribute(solidVerticesY, 3),
156
- );
157
- const yMaterial = new THREE.LineBasicMaterial({ color: colorY });
158
- this.add(new THREE.LineSegments(yGeometry, yMaterial));
159
- }
160
- }
161
-
162
- interface GridOptions {
163
- bbox: BoundingBox;
164
- ticks?: number;
165
- gridFontSize: number;
166
- centerGrid?: boolean;
167
- axes0?: boolean;
168
- grid: [boolean, boolean, boolean];
169
- flipY?: boolean;
170
- theme: Theme;
171
- cadWidth: number;
172
- height: number;
173
- maxAnisotropy: number;
174
- tickValueElement?: HTMLElement;
175
- tickInfoElement?: HTMLElement;
176
- getCamera: () => THREE.OrthographicCamera | THREE.PerspectiveCamera | null;
177
- getAxes0: () => boolean;
178
- onGridChange?: (allGrid: boolean, grids: [boolean, boolean, boolean]) => void;
179
- }
180
-
181
- /**
182
- * Grid component for displaying coordinate grids in 3D space.
183
- * Supports XY, XZ, and YZ plane grids with labeled tick marks.
184
- */
185
- class Grid extends THREE.Group {
186
- ticks: number;
187
- ticks0: number;
188
- gridFontSize: number;
189
- bbox: BoundingBox;
190
- centerGrid: boolean;
191
- axes0: boolean;
192
- grid: [boolean, boolean, boolean];
193
- allGrid: boolean;
194
- theme: Theme;
195
- flipY: boolean;
196
- lastZoomIndex: number;
197
- lastFontIndex: number;
198
- cadWidth: number;
199
- height: number;
200
- maxAnisotropy: number;
201
- tickValue: HTMLElement | null;
202
- info: HTMLElement | null;
203
- getCamera: () => THREE.OrthographicCamera | THREE.PerspectiveCamera | null;
204
- getAxes0: () => boolean;
205
- onGridChange: ((allGrid: boolean, grids: [boolean, boolean, boolean]) => void) | null;
206
- minFontIndex: number;
207
- minZoomIndex: number;
208
- zoomMaxIndex: number;
209
- canvasHeight: number;
210
- size: number;
211
- delta: number;
212
- geomCache: Record<string, THREE.CanvasTexture>;
213
- textureAspectRatios: Record<string, number>;
214
- labelCache: Record<string, THREE.Sprite>;
215
- materialCache: Record<string, THREE.SpriteMaterial>;
216
- colors: Record<Theme, string[]>;
217
-
218
- /**
219
- * Create a Grid instance
220
- * @param options - Configuration options
221
- */
222
- constructor(options: GridOptions) {
223
- super();
224
-
225
- // Validate required options
226
- const required: (keyof GridOptions)[] = [
227
- "bbox",
228
- "gridFontSize",
229
- "grid",
230
- "theme",
231
- "cadWidth",
232
- "height",
233
- "maxAnisotropy",
234
- "getCamera",
235
- "getAxes0",
236
- ];
237
- for (const key of required) {
238
- if (options[key] === undefined) {
239
- throw new Error(`Grid: required option "${key}" is missing`);
240
- }
241
- }
242
-
243
- const {
244
- bbox,
245
- ticks = 5,
246
- gridFontSize,
247
- centerGrid,
248
- axes0,
249
- grid,
250
- flipY,
251
- theme,
252
- cadWidth,
253
- height,
254
- maxAnisotropy,
255
- tickValueElement,
256
- tickInfoElement,
257
- getCamera,
258
- getAxes0,
259
- onGridChange,
260
- } = options;
261
-
262
- this.ticks = ticks;
263
- this.ticks0 = ticks;
264
- this.gridFontSize = gridFontSize;
265
- this.bbox = bbox;
266
- this.centerGrid = centerGrid || false;
267
- this.axes0 = axes0 || false;
268
- this.grid = grid;
269
- this.allGrid = !!(grid[0] || grid[1] || grid[2]);
270
- this.theme = theme;
271
- this.flipY = flipY || false;
272
- this.lastZoomIndex = 0;
273
- this.lastFontIndex = 50;
274
-
275
- // Store dimensions and renderer capability
276
- this.cadWidth = cadWidth;
277
- this.height = height;
278
- this.maxAnisotropy = maxAnisotropy;
279
-
280
- // Store DOM elements (optional)
281
- this.tickValue = tickValueElement || null;
282
- this.info = tickInfoElement || null;
283
-
284
- // Store callbacks for dynamic values
285
- this.getCamera = getCamera;
286
- this.getAxes0 = getAxes0;
287
- this.onGridChange = onGridChange || null;
288
-
289
- // Heuristics, experimentally determined
290
- const size = bbox.max_dist_from_center();
291
- const canvasSize = Math.min(cadWidth, height);
292
- const scale = Math.max(1.0, 6 - Math.log2(canvasSize / 100));
293
- this.minFontIndex = Math.round(
294
- (size < 2 ? 6 : size < 1000 ? 5 : 3) * scale,
295
- );
296
- this.minZoomIndex = -4;
297
- this.zoomMaxIndex = 5;
298
-
299
- this.canvasHeight = 128; // Fixed height for all label textures (higher = crisper)
300
-
301
- this.geomCache = {};
302
- this.textureAspectRatios = {}; // Store aspect ratio per texture
303
- this.labelCache = {};
304
- this.materialCache = {};
305
-
306
- this.size = 0;
307
- this.delta = 0;
308
-
309
- this.colors = {
310
- dark: [
311
- "#ff4500", // x
312
- "#32cd32", // y
313
- "#3b9eff", // z
314
- ],
315
- light: [
316
- "#ff4500", // x
317
- "#32cd32", // y
318
- "#3b9eff", // z
319
- ],
320
- };
321
-
322
- this.create();
323
- }
324
-
325
- /**
326
- * Calculate text scale based on camera mode and canvas size
327
- */
328
- private calculateTextScale(pixel: number): number {
329
- const camera = this.getCamera();
330
- // Guard against disposed viewer (camera may be null during cleanup)
331
- if (!camera) {
332
- return pixel;
333
- }
334
- const height = this.height;
335
-
336
- // Decrease fontsize for small canvases
337
- // 300px and below 80%
338
- // 800px and above 100%
339
- // linear in between
340
- const fontSize = cappedLinear(300, 0.8, 800, 1.0, height) * pixel;
341
-
342
- if (camera instanceof THREE.OrthographicCamera) {
343
- // Ortho: convert pixel size to world units based on zoom
344
- const visibleWorldHeight = (camera.top - camera.bottom) / camera.zoom;
345
- const pixelsPerWorldUnit = height / visibleWorldHeight;
346
-
347
- const scaleFactor = 1.6; // Adjust this to change ortho label size (1.0 = default, 2.0 = double)
348
- return (fontSize / pixelsPerWorldUnit) * scaleFactor;
349
- } else {
350
- // Perspective with sizeAttenuation: false
351
- // Scale is in normalized device coordinates (screen space)
352
- // Scale of 1.0 = full viewport height
353
- const scaleFactor = 0.6; // Adjust this to change label size (0.1 = smaller, 2.0 = larger)
354
- return (fontSize / height) * scaleFactor;
355
- }
356
- }
357
-
358
- /**
359
- * Update scale of all grid labels
360
- */
361
- scaleLabels(): void {
362
- for (const child of this.children) {
363
- if (!(child instanceof THREE.Group)) continue;
364
- for (let i = 1; i < child.children.length; i++) {
365
- const label = child.children[i];
366
- if (!(label instanceof THREE.Sprite)) continue;
367
- const s = this.calculateTextScale(this.gridFontSize);
368
- // Sprites need to maintain their individual aspect ratios
369
- const aspectRatio = label.userData.aspectRatio || 4; // fallback default
370
- label.scale.set(s * aspectRatio, s, 1);
371
- }
372
- }
373
- }
374
-
375
- /**
376
- * Show or hide all grid labels
377
- */
378
- private showLabels(flag: boolean): void {
379
- for (const child of this.children) {
380
- if (!(child instanceof THREE.Group)) continue;
381
- for (let i = 1; i < child.children.length; i++) {
382
- child.children[i].visible = flag;
383
- }
384
- }
385
- }
386
-
387
- /**
388
- * Update grid based on zoom level
389
- * @param zoom - Current zoom level
390
- * @param force - Force update regardless of zoom change
391
- * @param theme - Optional new theme to apply
392
- */
393
- async update(zoom: number, force: boolean = false, theme: Theme | null = null): Promise<void> {
394
- if (!this.getVisible()) return;
395
-
396
- // We got called from the change theme handler
397
- if (theme) this.theme = theme;
398
-
399
- let zoomIndex = Math.round(Math.log2(0.4 * zoom));
400
-
401
- if (Math.abs(zoomIndex) < 1e-6) zoomIndex = 0;
402
- if (
403
- force ||
404
- (zoomIndex != this.lastZoomIndex &&
405
- zoomIndex < this.zoomMaxIndex &&
406
- zoomIndex > this.minZoomIndex)
407
- ) {
408
- deepDispose(this.children);
409
- this.children = [];
410
-
411
- const halfTicks = (this.ticks0 / 2) * 2 ** zoomIndex;
412
- this.ticks = Math.round(2 * halfTicks);
413
-
414
- await this.create(false);
415
-
416
- this.lastZoomIndex = zoomIndex;
417
- force = true; // when grid is created newly, ensure font sizing is executed, too
418
- }
419
-
420
- const fontIndex = Math.round(zoom * 50);
421
- if (force || fontIndex != this.lastFontIndex) {
422
- if (fontIndex < this.minFontIndex) {
423
- this.showLabels(false);
424
- } else {
425
- // Only update scale in ortho mode
426
- // In perspective, sizeAttenuation handles scaling automatically
427
- if (this.getCamera() instanceof THREE.OrthographicCamera) {
428
- this.scaleLabels();
429
- }
430
- this.showLabels(true);
431
- }
432
- this.lastFontIndex = fontIndex;
433
- }
434
- }
435
-
436
- /**
437
- * Create the grid geometry and labels
438
- * @param nice - Whether to use nice bounds calculation
439
- */
440
- async create(nice: boolean = true): Promise<void> {
441
- // in case the bbox has the same size as the nice grid there should be
442
- // a margin bewteen grid and object. Hence factor 1.05
443
- if (nice) {
444
- const s2 = Math.max(
445
- Math.abs(this.bbox.max.x),
446
- Math.abs(this.bbox.max.y),
447
- Math.abs(this.bbox.max.z),
448
- Math.abs(this.bbox.min.x),
449
- Math.abs(this.bbox.min.y),
450
- Math.abs(this.bbox.min.z),
451
- );
452
- const [axisStart, axisEnd, niceTick] = this.niceBounds(
453
- -s2 * 1.05,
454
- s2 * 1.05,
455
- this.ticks,
456
- );
457
- this.size = axisEnd - axisStart;
458
- this.ticks = this.size / niceTick;
459
- this.ticks0 = this.ticks;
460
- this.delta = niceTick;
461
- } else {
462
- this.delta = this.size / this.ticks;
463
- }
464
- this.setTickInfo();
465
-
466
- for (let i = 0; i < 3; i++) {
467
- const group = new CompoundGroup();
468
- group.name = `GridHelper-${i}`;
469
- group.add(
470
- new GridHelper(
471
- this.size,
472
- 2 * this.ticks,
473
- this.colors[this.theme][i === 0 ? 1 : i === 1 ? 0 : 2],
474
- this.colors[this.theme][i === 0 ? 0 : i === 1 ? 2 : 1],
475
- this.theme == "dark" ? 0x7777777 : 0xbbbbbb,
476
- ),
477
- );
478
-
479
- let label: THREE.Sprite;
480
- for (let x = -this.size / 2; x <= this.size / 2; x += this.delta / 2) {
481
- if (Math.abs(x) < 1e-6) {
482
- continue;
483
- } // skip center label
484
-
485
- let x_fixed = trimTrailingZeros(x.toFixed(4));
486
- // Add '+' prefix for positive numbers
487
- if (x > 0) {
488
- x_fixed = "+" + x_fixed;
489
- }
490
-
491
- label = this.createLabel(x_fixed, x, i, true); //cached
492
- group.add(label);
493
-
494
- label = this.createLabel(x_fixed, x, i, false); //cached
495
- group.add(label);
496
- }
497
- this.add(group);
498
- }
499
- this.children[0].rotateX(Math.PI / 2);
500
- this.children[1].rotateY(Math.PI / 2);
501
- this.children[2].rotateZ(Math.PI / 2);
502
-
503
- this.setCenter(this.axes0, this.flipY);
504
- // Set initial scale (required for both modes)
505
- this.scaleLabels();
506
- this.setCenter(this.getAxes0(), this.flipY);
507
- this.setVisible();
508
- }
509
-
510
- /**
511
- * Create a text texture for grid labels
512
- */
513
- private createTextTexture(text: string): THREE.CanvasTexture {
514
- if (this.geomCache[text]) {
515
- return this.geomCache[text];
516
- }
517
-
518
- const canvas = document.createElement("canvas");
519
- const ctx = canvas.getContext("2d", {
520
- alpha: true,
521
- desynchronized: false,
522
- willReadFrequently: false,
523
- })!;
524
-
525
- // Use consistent high-quality settings regardless of text length
526
- const fontSize = 80;
527
- const strokeWidth = 12;
528
-
529
- const weight = this.theme === "dark" ? "500" : "560";
530
- const font = `${weight} ${fontSize}px Verdana, Arial, sans-serif`;
531
- ctx.font = font;
532
-
533
- // Measure text width to create appropriately sized canvas
534
- const metrics = ctx.measureText(text);
535
- const textWidth = metrics.width;
536
- const padding = 20;
537
-
538
- // Dynamic width for long text, consistent height for quality
539
- const canvasWidth = Math.round(textWidth + padding * 2);
540
- const canvasHeight = this.canvasHeight;
541
-
542
- canvas.width = canvasWidth;
543
- canvas.height = canvasHeight;
544
-
545
- // Need to reset context properties after canvas resize
546
- ctx.textRendering = "optimizeLegibility";
547
-
548
- ctx.font = font;
549
- ctx.textAlign = "center";
550
- ctx.textBaseline = "middle";
551
-
552
- // Clear with fully transparent background
553
- ctx.clearRect(0, 0, canvas.width, canvas.height);
554
-
555
- const centerX = canvas.width / 2;
556
- const centerY = canvas.height / 2;
557
-
558
- // Draw outline/stroke using actual canvas background color
559
- ctx.lineWidth = strokeWidth;
560
- ctx.lineJoin = "round";
561
- ctx.miterLimit = 2;
562
- ctx.strokeStyle = this.theme === "dark" ? "#444444" : "#ffffff";
563
- ctx.strokeText(text, centerX, centerY);
564
-
565
- // Draw main text on top
566
- ctx.fillStyle = this.theme === "dark" ? "#aaaaaa" : "#333333";
567
- ctx.fillText(text, centerX, centerY);
568
-
569
- const texture = new THREE.CanvasTexture(canvas);
570
- texture.needsUpdate = true;
571
-
572
- // Use LinearSRGBColorSpace for light theme, SRGBColorSpace for dark theme
573
- texture.colorSpace =
574
- this.theme === "dark" ? THREE.SRGBColorSpace : THREE.LinearSRGBColorSpace;
575
-
576
- // Use nearest filtering for crisp text
577
- texture.minFilter = THREE.LinearFilter;
578
- texture.magFilter = THREE.LinearFilter;
579
- texture.generateMipmaps = false;
580
- texture.anisotropy = this.maxAnisotropy;
581
- texture.premultiplyAlpha = false;
582
-
583
- // Clamp to edge to prevent sampling artifacts at borders
584
- texture.wrapS = THREE.ClampToEdgeWrapping;
585
- texture.wrapT = THREE.ClampToEdgeWrapping;
586
-
587
- // Store texture and its aspect ratio
588
- this.geomCache[text] = texture;
589
- this.textureAspectRatios[text] = canvasWidth / canvasHeight;
590
-
591
- return texture;
592
- }
593
-
594
- /**
595
- * Create a label sprite for grid axis
596
- */
597
- private createLabel(tick: string, x: number, i: number, horizontal: boolean): THREE.Sprite {
598
- const key = `${tick}_${i}_${horizontal}`;
599
- if (this.labelCache[key]) {
600
- const cached = this.labelCache[key];
601
- // Clone sprite - materials are shared per texture+plane+orientation
602
- // Sprite.material is typed as SpriteMaterial in THREE.js
603
- const sprite = new THREE.Sprite(cached.material);
604
- sprite.position.copy(cached.position);
605
- sprite.scale.copy(cached.scale);
606
- sprite.userData.aspectRatio = cached.userData.aspectRatio;
607
- return sprite;
608
- }
609
-
610
- const texture = this.createTextTexture(tick);
611
-
612
- // Determine rotation based on plane and axis
613
- // All labels should be perpendicular to their axis to prevent overlap
614
- // Ensure consistent rotation for each physical axis across all planes
615
- let rotation = 0;
616
- if (i === 0) {
617
- // XY plane: X-axis (horizontal) = 0°, Y-axis (vertical) = 0° for perpendicular
618
- rotation = 0;
619
- } else if (i === 1) {
620
- // XZ plane: Z-axis (horizontal) = 90°, X-axis (vertical) = 0° for perpendicular
621
- rotation = horizontal ? Math.PI / 2 : 0;
622
- } else {
623
- // YZ plane: Y-axis (horizontal) = 0°, Z-axis (vertical) = 90° (match above)
624
- rotation = horizontal ? 0 : Math.PI / 2;
625
- }
626
-
627
- // Create or reuse material based on texture and orientation
628
- const materialKey = `${tick}_${i}_${horizontal}`;
629
- let material = this.materialCache[materialKey];
630
- if (!material) {
631
- material = new THREE.SpriteMaterial({
632
- map: texture,
633
- transparent: true,
634
- depthTest: true,
635
- depthWrite: false,
636
- blending: THREE.NormalBlending,
637
- rotation: rotation,
638
- sizeAttenuation: false, // Disable distance scaling - maintain constant screen size
639
- });
640
- this.materialCache[materialKey] = material;
641
- }
642
-
643
- const sprite = new THREE.Sprite(material);
644
-
645
- // Adjust direction based on plane and axis to fix flipped labels
646
- let dir: number;
647
- if (i === 0) {
648
- // XY plane: vertical axis needs flip
649
- dir = horizontal ? 1 : -1;
650
- } else if (i === 1) {
651
- // XZ plane: horizontal axis needs flip (opposite of XY)
652
- dir = horizontal ? -1 : 1;
653
- } else {
654
- // YZ plane: no flip needed
655
- dir = 1;
656
- }
657
-
658
- if (horizontal) {
659
- sprite.position.set(dir * x, 0, 0);
660
- } else {
661
- sprite.position.set(0, 0, dir * x);
662
- }
663
-
664
- // Set initial scale using actual texture aspect ratio
665
- const aspectRatio = this.textureAspectRatios[tick] || 4; // fallback default
666
- sprite.scale.set(aspectRatio, 1, 1);
667
-
668
- // Store aspect ratio on sprite for scaleLabels to use
669
- sprite.userData.aspectRatio = aspectRatio;
670
-
671
- this.labelCache[key] = sprite;
672
- return sprite;
673
- }
674
-
675
- /**
676
- * Calculate nice symmetric grid bounds centered at zero
677
- */
678
- private niceBounds(axisStart: number, axisEnd: number, numTicks: number): [number, number, number] {
679
- if (!numTicks) {
680
- numTicks = 8;
681
- }
682
-
683
- // Calculate max absolute value (for symmetric grid)
684
- const maxAbsValue = Math.max(Math.abs(axisStart), Math.abs(axisEnd));
685
-
686
- if (maxAbsValue === 0) {
687
- return [0, 0, 0];
688
- }
689
-
690
- // Calculate rough delta
691
- const roughDelta = maxAbsValue / numTicks;
692
-
693
- // Find the order of magnitude
694
- const exponent = Math.floor(Math.log10(roughDelta));
695
- const magnitude = Math.pow(10, exponent);
696
-
697
- // Normalize to range [1, 10)
698
- const normalized = roughDelta / magnitude;
699
-
700
- // Round to nice number: 1, 2, 2.5, 5, or 10
701
- let niceFactor: number;
702
- if (normalized <= 1.0) {
703
- niceFactor = 1.0;
704
- } else if (normalized <= 2.0) {
705
- niceFactor = 2.0;
706
- } else if (normalized <= 2.5) {
707
- niceFactor = 2.5;
708
- } else if (normalized <= 5.0) {
709
- niceFactor = 5.0;
710
- } else {
711
- niceFactor = 10.0;
712
- }
713
-
714
- const niceDelta = niceFactor * magnitude;
715
-
716
- // Calculate how many ticks fit within the original bounds
717
- // Use Math.ceil to ensure we cover the full range
718
- const actualTicks = Math.ceil(maxAbsValue / niceDelta);
719
-
720
- // Calculate symmetric bounds based on actual ticks that fit
721
- const niceMax = niceDelta * actualTicks;
722
- const niceMin = -niceMax;
723
-
724
- return [niceMin, niceMax, niceDelta];
725
- }
726
-
727
- /**
728
- * Compute grid visibility and notify UI
729
- */
730
- computeGrid(): void {
731
- this.allGrid = !!(this.grid[0] || this.grid[1] || this.grid[2]);
732
-
733
- if (this.onGridChange) {
734
- this.onGridChange(this.allGrid, this.grid);
735
- }
736
-
737
- this.setVisible();
738
- }
739
-
740
- /**
741
- * Toggle grid visibility by action
742
- * @param action - Action type ("grid", "grid-xy", "grid-xz", "grid-yz")
743
- * @param flag - Optional explicit flag for "grid" action
744
- */
745
- setGrid(action: string, flag: boolean | null = null): void {
746
- switch (action) {
747
- case "grid":
748
- this.allGrid = flag == null ? !this.allGrid : flag;
749
- this.grid[0] = this.allGrid;
750
- this.grid[1] = this.allGrid;
751
- this.grid[2] = this.allGrid;
752
- break;
753
- case "grid-xy":
754
- this.grid[0] = !this.grid[0];
755
- break;
756
- case "grid-xz":
757
- this.grid[1] = !this.grid[1];
758
- break;
759
- case "grid-yz":
760
- this.grid[2] = !this.grid[2];
761
- break;
762
- }
763
- this.computeGrid();
764
- }
765
-
766
- /**
767
- * Set grid visibility for all planes
768
- * @param xy - XY plane visibility
769
- * @param xz - XZ plane visibility
770
- * @param yz - YZ plane visibility
771
- */
772
- setGrids(xy: boolean, xz: boolean, yz: boolean): void {
773
- this.grid[0] = xy;
774
- this.grid[1] = xz;
775
- this.grid[2] = yz;
776
- this.computeGrid();
777
- }
778
-
779
- /**
780
- * Set grid center position
781
- * @param axes0 - Whether to center at origin
782
- * @param flipY - Whether Y axis is flipped
783
- */
784
- setCenter(axes0: boolean, flipY: boolean): void {
785
- const c = axes0 ? [0, 0, 0] : this.bbox.center();
786
-
787
- this.children.forEach((ch) => ch.position.set(c[0], c[1], c[2]));
788
-
789
- if (!this.centerGrid) {
790
- this.children[0].position.z -= this.size / 2;
791
- this.children[1].position.y -= ((flipY ? -1 : 1) * this.size) / 2;
792
- this.children[2].position.x -= this.size / 2;
793
- }
794
- }
795
-
796
- /**
797
- * Update visibility of grid planes and tick info
798
- */
799
- setVisible(): void {
800
- this.children.forEach((ch, i) => {
801
- ch.visible = this.grid[i];
802
- });
803
- if (this.info) {
804
- this.info.style.display = this.allGrid ? "block" : "none";
805
- }
806
- }
807
-
808
- /**
809
- * Update tick info display
810
- */
811
- private setTickInfo(): void {
812
- if (this.tickValue) {
813
- this.tickValue.innerText = trimTrailingZeros((this.delta / 2).toFixed(4));
814
- }
815
- }
816
-
817
- /**
818
- * Get overall grid visibility
819
- * @returns Whether any grid plane is visible
820
- */
821
- getVisible(): boolean {
822
- return this.allGrid;
823
- }
824
-
825
- /**
826
- * Clear all caches (textures, materials, labels)
827
- */
828
- clearCache(): void {
829
- // Dispose textures from geomCache
830
- if (Object.keys(this.geomCache).length > 0) {
831
- for (const key of Object.keys(this.geomCache)) {
832
- const texture = this.geomCache[key];
833
- texture.dispose();
834
- }
835
- this.geomCache = {};
836
- }
837
-
838
- // Clear texture aspect ratios
839
- this.textureAspectRatios = {};
840
-
841
- // Dispose materials from materialCache
842
- if (this.materialCache && Object.keys(this.materialCache).length > 0) {
843
- for (const key of Object.keys(this.materialCache)) {
844
- const material = this.materialCache[key];
845
- material.dispose();
846
- }
847
- this.materialCache = {};
848
- }
849
-
850
- // Clear labelCache (sprites reference shared materials, so no disposal needed here)
851
- if (Object.keys(this.labelCache).length > 0) {
852
- this.labelCache = {};
853
- }
854
- }
855
-
856
- /**
857
- * Dispose all resources
858
- */
859
- dispose(): void {
860
- this.clearCache();
861
- }
862
- }
863
-
864
- export { Grid };