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.
- package/dist/scene/clipping.d.ts +6 -0
- package/dist/three-cad-viewer.esm.js +20 -5
- package/dist/three-cad-viewer.esm.js.map +1 -1
- package/dist/three-cad-viewer.esm.min.js +1 -1
- package/dist/three-cad-viewer.js +20 -5
- package/dist/three-cad-viewer.min.js +1 -1
- package/package.json +2 -3
- package/src/_version.ts +0 -1
- package/src/camera/camera.ts +0 -445
- package/src/camera/controls/CADOrbitControls.ts +0 -241
- package/src/camera/controls/CADTrackballControls.ts +0 -598
- package/src/camera/controls.ts +0 -380
- package/src/core/patches.ts +0 -16
- package/src/core/studio-manager.ts +0 -652
- package/src/core/types.ts +0 -892
- package/src/core/viewer-state.ts +0 -784
- package/src/core/viewer.ts +0 -4821
- package/src/index.ts +0 -151
- package/src/rendering/environment.ts +0 -840
- package/src/rendering/light-detection.ts +0 -327
- package/src/rendering/material-factory.ts +0 -735
- package/src/rendering/material-presets.ts +0 -289
- package/src/rendering/raycast.ts +0 -291
- package/src/rendering/room-environment.ts +0 -192
- package/src/rendering/studio-composer.ts +0 -577
- package/src/rendering/studio-floor.ts +0 -108
- package/src/rendering/texture-cache.ts +0 -324
- package/src/rendering/tree-model.ts +0 -542
- package/src/rendering/triplanar.ts +0 -329
- package/src/scene/animation.ts +0 -343
- package/src/scene/axes.ts +0 -108
- package/src/scene/bbox.ts +0 -223
- package/src/scene/clipping.ts +0 -640
- package/src/scene/grid.ts +0 -864
- package/src/scene/nestedgroup.ts +0 -1444
- package/src/scene/objectgroup.ts +0 -866
- package/src/scene/orientation.ts +0 -259
- package/src/scene/render-shape.ts +0 -634
- package/src/tools/cad_tools/measure.ts +0 -811
- package/src/tools/cad_tools/select.ts +0 -100
- package/src/tools/cad_tools/tools.ts +0 -231
- package/src/tools/cad_tools/ui.ts +0 -454
- package/src/tools/cad_tools/zebra.ts +0 -369
- package/src/types/html.d.ts +0 -5
- package/src/types/n8ao.d.ts +0 -28
- package/src/types/three-augmentation.d.ts +0 -60
- package/src/ui/display.ts +0 -3295
- package/src/ui/index.html +0 -505
- package/src/ui/info.ts +0 -177
- package/src/ui/slider.ts +0 -206
- package/src/ui/toolbar.ts +0 -347
- package/src/ui/treeview.ts +0 -945
- package/src/utils/decode-instances.ts +0 -233
- package/src/utils/font.ts +0 -60
- package/src/utils/gpu-tracker.ts +0 -265
- package/src/utils/logger.ts +0 -92
- package/src/utils/sizeof.ts +0 -116
- package/src/utils/timer.ts +0 -69
- 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 };
|