@chocozhang/three-model-render 1.0.2 → 1.0.4

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 (42) hide show
  1. package/README.md +149 -530
  2. package/dist/camera/index.d.ts +59 -36
  3. package/dist/camera/index.js +77 -57
  4. package/dist/camera/index.js.map +1 -1
  5. package/dist/camera/index.mjs +77 -57
  6. package/dist/camera/index.mjs.map +1 -1
  7. package/dist/core/index.d.ts +60 -27
  8. package/dist/core/index.js +124 -95
  9. package/dist/core/index.js.map +1 -1
  10. package/dist/core/index.mjs +124 -95
  11. package/dist/core/index.mjs.map +1 -1
  12. package/dist/effect/index.d.ts +47 -134
  13. package/dist/effect/index.js +109 -65
  14. package/dist/effect/index.js.map +1 -1
  15. package/dist/effect/index.mjs +109 -65
  16. package/dist/effect/index.mjs.map +1 -1
  17. package/dist/index.d.ts +397 -341
  18. package/dist/index.js +651 -472
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.mjs +651 -472
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/interaction/index.d.ts +85 -52
  23. package/dist/interaction/index.js +161 -133
  24. package/dist/interaction/index.js.map +1 -1
  25. package/dist/interaction/index.mjs +161 -133
  26. package/dist/interaction/index.mjs.map +1 -1
  27. package/dist/loader/index.d.ts +89 -56
  28. package/dist/loader/index.js +115 -76
  29. package/dist/loader/index.js.map +1 -1
  30. package/dist/loader/index.mjs +115 -76
  31. package/dist/loader/index.mjs.map +1 -1
  32. package/dist/setup/index.d.ts +28 -18
  33. package/dist/setup/index.js +33 -24
  34. package/dist/setup/index.js.map +1 -1
  35. package/dist/setup/index.mjs +33 -24
  36. package/dist/setup/index.mjs.map +1 -1
  37. package/dist/ui/index.d.ts +18 -7
  38. package/dist/ui/index.js +32 -22
  39. package/dist/ui/index.js.map +1 -1
  40. package/dist/ui/index.mjs +32 -22
  41. package/dist/ui/index.mjs.map +1 -1
  42. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -8,25 +8,35 @@ import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUti
8
8
  import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js';
9
9
 
10
10
  /**
11
- * FBX 子模型添加头顶标签(支持 Mesh 和 Group)- 优化版
11
+ * @file labelManager.ts
12
+ * @description
13
+ * Manages HTML labels attached to 3D objects. Efficiently updates label positions based on camera movement.
12
14
  *
13
- * ✨ 性能优化:
14
- * - 缓存包围盒,避免每帧重复计算
15
- * - 支持暂停/恢复更新
16
- * - 可配置更新间隔,降低 CPU 占用
17
- * - 只在可见时更新,隐藏时自动暂停
15
+ * @best-practice
16
+ * - Use `addChildModelLabels` to label parts of a loaded model.
17
+ * - Labels are HTML elements overlaid on the canvas.
18
+ * - Supports performance optimization via caching and visibility culling.
19
+ */
20
+ /**
21
+ * Add overhead labels to child models (supports Mesh and Group)
22
+ *
23
+ * Features:
24
+ * - Caches bounding boxes to avoid repetitive calculation every frame
25
+ * - Supports pause/resume
26
+ * - Configurable update interval to reduce CPU usage
27
+ * - Automatically pauses when hidden
18
28
  *
19
- * @param camera THREE.Camera - 场景摄像机
20
- * @param renderer THREE.WebGLRenderer - 渲染器,用于屏幕尺寸
21
- * @param parentModel THREE.Object3D - FBX 根节点或 Group
22
- * @param modelLabelsMap Record<string,string> - 模型 name 标签文字 映射表
23
- * @param options LabelOptions - 可选标签样式配置
24
- * @returns LabelManager - 包含 pause/resume/dispose 的管理接口
29
+ * @param camera THREE.Camera - Scene camera
30
+ * @param renderer THREE.WebGLRenderer - Renderer, used for screen size
31
+ * @param parentModel THREE.Object3D - FBX root node or Group
32
+ * @param modelLabelsMap Record<string,string> - Map of model name to label text
33
+ * @param options LabelOptions - Optional label style configuration
34
+ * @returns LabelManager - Management interface containing pause/resume/dispose
25
35
  */
26
36
  function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, options) {
27
- // 防御性检查:确保 parentModel 已加载
37
+ // Defensive check: ensure parentModel is loaded
28
38
  if (!parentModel || typeof parentModel.traverse !== 'function') {
29
- console.error('parentModel 无效,请确保 FBX 模型已加载完成');
39
+ console.error('parentModel invalid, please ensure the FBX model is loaded');
30
40
  return {
31
41
  pause: () => { },
32
42
  resume: () => { },
@@ -34,48 +44,48 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
34
44
  isRunning: () => false
35
45
  };
36
46
  }
37
- // 配置项
47
+ // Configuration
38
48
  const enableCache = (options === null || options === void 0 ? void 0 : options.enableCache) !== false;
39
49
  const updateInterval = (options === null || options === void 0 ? void 0 : options.updateInterval) || 0;
40
- // 创建标签容器,绝对定位,放在 body
50
+ // Create label container, absolute positioning, attached to body
41
51
  const container = document.createElement('div');
42
52
  container.style.position = 'absolute';
43
53
  container.style.top = '0';
44
54
  container.style.left = '0';
45
- container.style.pointerEvents = 'none'; // 避免阻挡鼠标事件
55
+ container.style.pointerEvents = 'none'; // Avoid blocking mouse events
46
56
  container.style.zIndex = '1000';
47
57
  document.body.appendChild(container);
48
58
  const labels = [];
49
- // 状态管理
59
+ // State management
50
60
  let rafId = null;
51
61
  let isPaused = false;
52
62
  let lastUpdateTime = 0;
53
- // 遍历所有子模型
63
+ // Traverse all child models
54
64
  parentModel.traverse((child) => {
55
65
  var _a;
56
- // 只处理 Mesh Group
66
+ // Only process Mesh or Group
57
67
  if ((child.isMesh || child.type === 'Group')) {
58
- // 动态匹配 name,防止 undefined
68
+ // Dynamic matching of name to prevent undefined
59
69
  const labelText = (_a = Object.entries(modelLabelsMap).find(([key]) => child.name.includes(key))) === null || _a === void 0 ? void 0 : _a[1];
60
70
  if (!labelText)
61
- return; // 没有匹配标签则跳过
62
- // 创建 DOM 标签
71
+ return; // Skip if no matching label
72
+ // Create DOM label
63
73
  const el = document.createElement('div');
64
74
  el.innerText = labelText;
65
- // 样式直接在 JS 中定义,可通过 options 覆盖
75
+ // Styles defined in JS, can be overridden via options
66
76
  el.style.position = 'absolute';
67
77
  el.style.color = (options === null || options === void 0 ? void 0 : options.color) || '#fff';
68
78
  el.style.background = (options === null || options === void 0 ? void 0 : options.background) || 'rgba(0,0,0,0.6)';
69
79
  el.style.padding = (options === null || options === void 0 ? void 0 : options.padding) || '4px 8px';
70
80
  el.style.borderRadius = (options === null || options === void 0 ? void 0 : options.borderRadius) || '4px';
71
81
  el.style.fontSize = (options === null || options === void 0 ? void 0 : options.fontSize) || '14px';
72
- el.style.transform = 'translate(-50%, -100%)'; // 让标签在模型正上方
82
+ el.style.transform = 'translate(-50%, -100%)'; // Position label directly above the model
73
83
  el.style.whiteSpace = 'nowrap';
74
84
  el.style.pointerEvents = 'none';
75
85
  el.style.transition = 'opacity 0.2s ease';
76
- // 加入容器
86
+ // Append to container
77
87
  container.appendChild(el);
78
- // 初始化缓存
88
+ // Initialize cache
79
89
  const cachedBox = new THREE.Box3().setFromObject(child);
80
90
  const center = new THREE.Vector3();
81
91
  cachedBox.getCenter(center);
@@ -90,7 +100,7 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
90
100
  }
91
101
  });
92
102
  /**
93
- * 更新缓存的包围盒(仅在模型变换时调用)
103
+ * Update cached bounding box (called only when model transforms)
94
104
  */
95
105
  const updateCache = (labelData) => {
96
106
  labelData.cachedBox.setFromObject(labelData.object);
@@ -100,18 +110,18 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
100
110
  labelData.needsUpdate = false;
101
111
  };
102
112
  /**
103
- * 获取对象顶部世界坐标(使用缓存)
113
+ * Get object top world coordinates (using cache)
104
114
  */
105
115
  const getObjectTopPosition = (labelData) => {
106
116
  if (enableCache) {
107
- // 检查对象是否发生变换
117
+ // Check if object has transformed
108
118
  if (labelData.needsUpdate || labelData.object.matrixWorldNeedsUpdate) {
109
119
  updateCache(labelData);
110
120
  }
111
121
  return labelData.cachedTopPos;
112
122
  }
113
123
  else {
114
- // 不使用缓存,每次都重新计算
124
+ // Do not use cache, recalculate every time
115
125
  const box = new THREE.Box3().setFromObject(labelData.object);
116
126
  const center = new THREE.Vector3();
117
127
  box.getCenter(center);
@@ -119,15 +129,15 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
119
129
  }
120
130
  };
121
131
  /**
122
- * 更新标签位置函数
132
+ * Update label positions function
123
133
  */
124
134
  function updateLabels(timestamp = 0) {
125
- // 检查是否暂停
135
+ // Check pause state
126
136
  if (isPaused) {
127
137
  rafId = null;
128
138
  return;
129
139
  }
130
- // 检查更新间隔
140
+ // Check update interval
131
141
  if (updateInterval > 0 && timestamp - lastUpdateTime < updateInterval) {
132
142
  rafId = requestAnimationFrame(updateLabels);
133
143
  return;
@@ -137,22 +147,22 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
137
147
  const height = renderer.domElement.clientHeight;
138
148
  labels.forEach((labelData) => {
139
149
  const { el } = labelData;
140
- const pos = getObjectTopPosition(labelData); // 使用缓存的顶部坐标
141
- pos.project(camera); // 转到屏幕坐标
142
- const x = (pos.x * 0.5 + 0.5) * width; // 屏幕 X
143
- const y = (-(pos.y * 0.5) + 0.5) * height; // 屏幕 Y
144
- // 控制标签显示/隐藏(摄像机后方隐藏)
150
+ const pos = getObjectTopPosition(labelData); // Use cached top position
151
+ pos.project(camera); // Convert to screen coordinates
152
+ const x = (pos.x * 0.5 + 0.5) * width; // Screen X
153
+ const y = (-(pos.y * 0.5) + 0.5) * height; // Screen Y
154
+ // Control label visibility (hidden when behind camera)
145
155
  const isVisible = pos.z < 1;
146
156
  el.style.opacity = isVisible ? '1' : '0';
147
157
  el.style.display = isVisible ? 'block' : 'none';
148
- el.style.transform = `translate(-50%, -100%) translate(${x}px, ${y}px)`; // 屏幕位置
158
+ el.style.transform = `translate(-50%, -100%) translate(${x}px, ${y}px)`; // Screen position
149
159
  });
150
- rafId = requestAnimationFrame(updateLabels); // 循环更新
160
+ rafId = requestAnimationFrame(updateLabels); // Loop update
151
161
  }
152
- // 启动更新
162
+ // Start update
153
163
  updateLabels();
154
164
  /**
155
- * 暂停更新
165
+ * Pause updates
156
166
  */
157
167
  const pause = () => {
158
168
  isPaused = true;
@@ -162,7 +172,7 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
162
172
  }
163
173
  };
164
174
  /**
165
- * 恢复更新
175
+ * Resume updates
166
176
  */
167
177
  const resume = () => {
168
178
  if (!isPaused)
@@ -171,11 +181,11 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
171
181
  updateLabels();
172
182
  };
173
183
  /**
174
- * 检查是否正在运行
184
+ * Check if running
175
185
  */
176
186
  const isRunning = () => !isPaused;
177
187
  /**
178
- * 清理函数:卸载所有 DOM 标签,取消动画,避免内存泄漏
188
+ * Cleanup function: Remove all DOM labels, cancel animation, avoid memory leaks
179
189
  */
180
190
  const dispose = () => {
181
191
  pause();
@@ -197,36 +207,45 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
197
207
  };
198
208
  }
199
209
 
200
- // src/utils/hoverBreathEffectByNameSingleton.ts
201
210
  /**
202
- * 创建单例高亮器 —— 推荐在 mounted 时创建一次
203
- * 返回 { updateHighlightNames, dispose, getHoveredName } 接口
211
+ * @file hoverEffect.ts
212
+ * @description
213
+ * Singleton highlight effect manager. Uses OutlinePass to create a breathing highlight effect on hovered objects.
214
+ *
215
+ * @best-practice
216
+ * - Initialize once in your setup/mounted hook.
217
+ * - Call `updateHighlightNames` to filter which objects are interactive.
218
+ * - Automatically handles mousemove throttling and cleanup on dispose.
219
+ */
220
+ /**
221
+ * Create a singleton highlighter - Recommended to create once on mount
222
+ * Returns { updateHighlightNames, dispose, getHoveredName } interface
204
223
  *
205
- * ✨ 性能优化:
206
- * - hover 对象时自动暂停动画
207
- * - mousemove 节流处理,避免过度计算
208
- * - 使用 passive 事件监听器提升滚动性能
224
+ * Features:
225
+ * - Automatically pauses animation when no object is hovered
226
+ * - Throttles mousemove events to avoid excessive calculation
227
+ * - Uses passive event listeners to improve scrolling performance
209
228
  */
210
229
  function enableHoverBreath(opts) {
211
- const { camera, scene, renderer, outlinePass, highlightNames = null, minStrength = 2, maxStrength = 5, speed = 4, throttleDelay = 16, // 默认约 60fps
230
+ const { camera, scene, renderer, outlinePass, highlightNames = null, minStrength = 2, maxStrength = 5, speed = 4, throttleDelay = 16, // Default ~60fps
212
231
  } = opts;
213
232
  const raycaster = new THREE.Raycaster();
214
233
  const mouse = new THREE.Vector2();
215
234
  let hovered = null;
216
235
  let time = 0;
217
236
  let animationId = null;
218
- // highlightSet: null 表示 all; empty Set 表示 none
237
+ // highlightSet: null means all; empty Set means none
219
238
  let highlightSet = highlightNames === null ? null : new Set(highlightNames);
220
- // 节流相关
239
+ // Throttling related
221
240
  let lastMoveTime = 0;
222
241
  let rafPending = false;
223
242
  function setHighlightNames(names) {
224
243
  highlightSet = names === null ? null : new Set(names);
225
- // 如果当前 hovered 不在新名单中,及时清理 selection
244
+ // If current hovered object is not in the new list, clean up selection immediately
226
245
  if (hovered && highlightSet && !highlightSet.has(hovered.name)) {
227
246
  hovered = null;
228
247
  outlinePass.selectedObjects = [];
229
- // 暂停动画
248
+ // Pause animation
230
249
  if (animationId !== null) {
231
250
  cancelAnimationFrame(animationId);
232
251
  animationId = null;
@@ -234,13 +253,13 @@ function enableHoverBreath(opts) {
234
253
  }
235
254
  }
236
255
  /**
237
- * 节流版本的 mousemove 处理
256
+ * Throttled mousemove handler
238
257
  */
239
258
  function onMouseMove(ev) {
240
259
  const now = performance.now();
241
- // 节流:如果距上次处理时间小于阈值,跳过
260
+ // Throttle: if time since last process is less than threshold, skip
242
261
  if (now - lastMoveTime < throttleDelay) {
243
- // 使用 RAF 延迟处理,避免丢失最后一次事件
262
+ // Use RAF to process the latest event later, ensuring the last event isn't lost
244
263
  if (!rafPending) {
245
264
  rafPending = true;
246
265
  requestAnimationFrame(() => {
@@ -254,24 +273,24 @@ function enableHoverBreath(opts) {
254
273
  processMouseMove(ev);
255
274
  }
256
275
  /**
257
- * 实际的 mousemove 逻辑
276
+ * Actual mousemove logic
258
277
  */
259
278
  function processMouseMove(ev) {
260
279
  const rect = renderer.domElement.getBoundingClientRect();
261
280
  mouse.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
262
281
  mouse.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
263
282
  raycaster.setFromCamera(mouse, camera);
264
- // 深度检测 scene 的所有子对象(true
283
+ // Deep detect all children of the scene (true)
265
284
  const intersects = raycaster.intersectObjects(scene.children, true);
266
285
  if (intersects.length > 0) {
267
286
  const obj = intersects[0].object;
268
- // 判断是否允许被高亮
287
+ // Determine if it is allowed to be highlighted
269
288
  const allowed = highlightSet === null ? true : highlightSet.has(obj.name);
270
289
  if (allowed) {
271
290
  if (hovered !== obj) {
272
291
  hovered = obj;
273
292
  outlinePass.selectedObjects = [obj];
274
- // 启动动画(如果未运行)
293
+ // Start animation (if not running)
275
294
  if (animationId === null) {
276
295
  animate();
277
296
  }
@@ -281,7 +300,7 @@ function enableHoverBreath(opts) {
281
300
  if (hovered !== null) {
282
301
  hovered = null;
283
302
  outlinePass.selectedObjects = [];
284
- // 停止动画
303
+ // Stop animation
285
304
  if (animationId !== null) {
286
305
  cancelAnimationFrame(animationId);
287
306
  animationId = null;
@@ -293,7 +312,7 @@ function enableHoverBreath(opts) {
293
312
  if (hovered !== null) {
294
313
  hovered = null;
295
314
  outlinePass.selectedObjects = [];
296
- // 停止动画
315
+ // Stop animation
297
316
  if (animationId !== null) {
298
317
  cancelAnimationFrame(animationId);
299
318
  animationId = null;
@@ -302,10 +321,10 @@ function enableHoverBreath(opts) {
302
321
  }
303
322
  }
304
323
  /**
305
- * 动画循环 - 只在有 hovered 对象时运行
324
+ * Animation loop - only runs when there is a hovered object
306
325
  */
307
326
  function animate() {
308
- // 如果没有 hovered 对象,停止动画
327
+ // If no hovered object, stop animation
309
328
  if (!hovered) {
310
329
  animationId = null;
311
330
  return;
@@ -315,11 +334,11 @@ function enableHoverBreath(opts) {
315
334
  const strength = minStrength + ((Math.sin(time) + 1) / 2) * (maxStrength - minStrength);
316
335
  outlinePass.edgeStrength = strength;
317
336
  }
318
- // 启动(只调用一次)
319
- // 使用 passive 提升滚动性能
337
+ // Start (called only once)
338
+ // Use passive to improve scrolling performance
320
339
  renderer.domElement.addEventListener('mousemove', onMouseMove, { passive: true });
321
- // 注意:不在这里启动 animate,等有 hover 对象时再启动
322
- // refresh: 如果你在某些情况下需要强制清理 selectedObjects
340
+ // Note: Do not start animate here, wait until there is a hover object
341
+ // refresh: Forcibly clean up selectedObjects if needed
323
342
  function refreshSelection() {
324
343
  if (hovered && highlightSet && !highlightSet.has(hovered.name)) {
325
344
  hovered = null;
@@ -340,7 +359,7 @@ function enableHoverBreath(opts) {
340
359
  animationId = null;
341
360
  }
342
361
  outlinePass.selectedObjects = [];
343
- // 清空引用
362
+ // Clear references
344
363
  hovered = null;
345
364
  highlightSet = null;
346
365
  }
@@ -353,23 +372,33 @@ function enableHoverBreath(opts) {
353
372
  }
354
373
 
355
374
  /**
356
- * 初始化描边相关信息(包含 OutlinePass)- 优化版
375
+ * @file postProcessing.ts
376
+ * @description
377
+ * Manages the post-processing chain, specifically for Outline effects and Gamma correction.
357
378
  *
358
- * ✨ 功能增强:
359
- * - 支持窗口 resize 自动更新
360
- * - 可配置分辨率缩放提升性能
361
- * - 完善的资源释放管理
379
+ * @best-practice
380
+ * - call `initPostProcessing` after creating your renderer and scene.
381
+ * - Use the returned `composer` in your render loop instead of `renderer.render`.
382
+ * - Handles resizing automatically via the `resize` method.
383
+ */
384
+ /**
385
+ * Initialize outline-related information (contains OutlinePass)
386
+ *
387
+ * Capabilities:
388
+ * - Supports automatic update on window resize
389
+ * - Configurable resolution scale for performance improvement
390
+ * - Comprehensive resource disposal management
362
391
  *
363
392
  * @param renderer THREE.WebGLRenderer
364
393
  * @param scene THREE.Scene
365
394
  * @param camera THREE.Camera
366
- * @param options PostProcessingOptions - 可选配置
367
- * @returns PostProcessingManager - 包含 composer/outlinePass/resize/dispose 的管理接口
395
+ * @param options PostProcessingOptions - Optional configuration
396
+ * @returns PostProcessingManager - Management interface containing composer/outlinePass/resize/dispose
368
397
  */
369
398
  function initPostProcessing(renderer, scene, camera, options = {}) {
370
- // 默认配置
399
+ // Default configuration
371
400
  const { edgeStrength = 4, edgeGlow = 1, edgeThickness = 2, visibleEdgeColor = '#ffee00', hiddenEdgeColor = '#000000', resolutionScale = 1.0 } = options;
372
- // 获取渲染器实际尺寸
401
+ // Get renderer actual size
373
402
  const getSize = () => {
374
403
  const width = renderer.domElement.clientWidth;
375
404
  const height = renderer.domElement.clientHeight;
@@ -379,12 +408,12 @@ function initPostProcessing(renderer, scene, camera, options = {}) {
379
408
  };
380
409
  };
381
410
  const size = getSize();
382
- // 创建 EffectComposer
411
+ // Create EffectComposer
383
412
  const composer = new EffectComposer(renderer);
384
- // 基础 RenderPass
413
+ // Basic RenderPass
385
414
  const renderPass = new RenderPass(scene, camera);
386
415
  composer.addPass(renderPass);
387
- // OutlinePass 用于模型描边
416
+ // OutlinePass for model outlining
388
417
  const outlinePass = new OutlinePass(new THREE.Vector2(size.width, size.height), scene, camera);
389
418
  outlinePass.edgeStrength = edgeStrength;
390
419
  outlinePass.edgeGlow = edgeGlow;
@@ -392,34 +421,34 @@ function initPostProcessing(renderer, scene, camera, options = {}) {
392
421
  outlinePass.visibleEdgeColor.set(visibleEdgeColor);
393
422
  outlinePass.hiddenEdgeColor.set(hiddenEdgeColor);
394
423
  composer.addPass(outlinePass);
395
- // Gamma 校正
424
+ // Gamma correction
396
425
  const gammaPass = new ShaderPass(GammaCorrectionShader);
397
426
  composer.addPass(gammaPass);
398
427
  /**
399
- * resize 处理函数
400
- * @param width 可选宽度,不传则使用 renderer.domElement 的实际宽度
401
- * @param height 可选高度,不传则使用 renderer.domElement 的实际高度
428
+ * Handle resize
429
+ * @param width Optional width, uses renderer.domElement actual width if not provided
430
+ * @param height Optional height, uses renderer.domElement actual height if not provided
402
431
  */
403
432
  const resize = (width, height) => {
404
433
  const actualSize = width !== undefined && height !== undefined
405
434
  ? { width: Math.floor(width * resolutionScale), height: Math.floor(height * resolutionScale) }
406
435
  : getSize();
407
- // 更新 composer 尺寸
436
+ // Update composer size
408
437
  composer.setSize(actualSize.width, actualSize.height);
409
- // 更新 outlinePass 分辨率
438
+ // Update outlinePass resolution
410
439
  outlinePass.resolution.set(actualSize.width, actualSize.height);
411
440
  };
412
441
  /**
413
- * 释放资源
442
+ * Dispose resources
414
443
  */
415
444
  const dispose = () => {
416
- // 释放所有 passes
445
+ // Dipose all passes
417
446
  composer.passes.forEach(pass => {
418
447
  if (pass.dispose) {
419
448
  pass.dispose();
420
449
  }
421
450
  });
422
- // 清空 passes 数组
451
+ // Clear passes array
423
452
  composer.passes.length = 0;
424
453
  };
425
454
  return {
@@ -431,28 +460,38 @@ function initPostProcessing(renderer, scene, camera, options = {}) {
431
460
  }
432
461
 
433
462
  /**
434
- * 创建模型点击高亮工具(OutlinePass 版)- 优化版
463
+ * @file clickHandler.ts
464
+ * @description
465
+ * Tool for handling model clicks and highlighting (OutlinePass version).
466
+ *
467
+ * @best-practice
468
+ * - Use `createModelClickHandler` to setup interaction.
469
+ * - Handles debouncing and click threshold automatically.
470
+ * - Cleanup using the returned dispose function.
471
+ */
472
+ /**
473
+ * Create Model Click Highlight Tool (OutlinePass Version) - Optimized
435
474
  *
436
- * ✨ 功能增强:
437
- * - 使用 AbortController 统一管理事件生命周期
438
- * - 支持防抖处理避免频繁触发
439
- * - 可自定义 Raycaster 参数
440
- * - 根据相机距离动态调整描边厚度
475
+ * Features:
476
+ * - Uses AbortController to unify event lifecycle management
477
+ * - Supports debounce to avoid frequent triggering
478
+ * - Customizable Raycaster parameters
479
+ * - Dynamically adjusts outline thickness based on camera distance
441
480
  *
442
- * @param camera 相机
443
- * @param scene 场景
444
- * @param renderer 渲染器
445
- * @param outlinePass 已初始化的 OutlinePass
446
- * @param onClick 点击回调
447
- * @param options 可选配置
448
- * @returns dispose 函数,用于清理事件和资源
481
+ * @param camera Camera
482
+ * @param scene Scene
483
+ * @param renderer Renderer
484
+ * @param outlinePass Initialized OutlinePass
485
+ * @param onClick Click callback
486
+ * @param options Optional configuration
487
+ * @returns Dispose function, used to clean up events and resources
449
488
  */
450
489
  function createModelClickHandler(camera, scene, renderer, outlinePass, onClick, options = {}) {
451
- // 配置项
490
+ // Configuration
452
491
  const { clickThreshold = 3, debounceDelay = 0, raycasterParams = {}, enableDynamicThickness = true, minThickness = 1, maxThickness = 10 } = options;
453
492
  const raycaster = new THREE.Raycaster();
454
493
  const mouse = new THREE.Vector2();
455
- // 应用 raycaster 自定义参数
494
+ // Apply Raycaster custom parameters
456
495
  if (raycasterParams.near !== undefined)
457
496
  raycaster.near = raycasterParams.near;
458
497
  if (raycasterParams.far !== undefined)
@@ -469,25 +508,25 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
469
508
  let startY = 0;
470
509
  let selectedObject = null;
471
510
  let debounceTimer = null;
472
- // 使用 AbortController 统一管理事件
511
+ // Use AbortController to manage events uniformly
473
512
  const abortController = new AbortController();
474
513
  const signal = abortController.signal;
475
- /** 恢复对象高亮(清空 OutlinePass.selectedObjects */
514
+ /** Restore object highlight (Clear OutlinePass.selectedObjects) */
476
515
  function restoreObject() {
477
516
  outlinePass.selectedObjects = [];
478
517
  }
479
- /** 鼠标按下记录位置 */
518
+ /** Record mouse down position */
480
519
  function handleMouseDown(event) {
481
520
  startX = event.clientX;
482
521
  startY = event.clientY;
483
522
  }
484
- /** 鼠标抬起判定点击或拖动(带防抖) */
523
+ /** Mouse up determines click or drag (with debounce) */
485
524
  function handleMouseUp(event) {
486
525
  const dx = Math.abs(event.clientX - startX);
487
526
  const dy = Math.abs(event.clientY - startY);
488
527
  if (dx > clickThreshold || dy > clickThreshold)
489
- return; // 拖动不触发点击
490
- // 防抖处理
528
+ return; // Drag does not trigger click
529
+ // Debounce processing
491
530
  if (debounceDelay > 0) {
492
531
  if (debounceTimer !== null) {
493
532
  clearTimeout(debounceTimer);
@@ -501,7 +540,7 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
501
540
  processClick(event);
502
541
  }
503
542
  }
504
- /** 实际的点击处理逻辑 */
543
+ /** Actual click processing logic */
505
544
  function processClick(event) {
506
545
  const rect = renderer.domElement.getBoundingClientRect();
507
546
  mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
@@ -510,55 +549,64 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
510
549
  const intersects = raycaster.intersectObjects(scene.children, true);
511
550
  if (intersects.length > 0) {
512
551
  let object = intersects[0].object;
513
- // 点击不同模型,先清除之前高亮
552
+ // Click different model, clear previous highlight first
514
553
  if (selectedObject && selectedObject !== object)
515
554
  restoreObject();
516
555
  selectedObject = object;
517
- // highlightObject(selectedObject); // 可选:是否自动高亮
556
+ // highlightObject(selectedObject); // Optional: whether to auto highlight
518
557
  onClick(selectedObject, {
519
- name: selectedObject.name || '未命名模型',
558
+ name: selectedObject.name || 'Unnamed Model',
520
559
  position: selectedObject.getWorldPosition(new THREE.Vector3()),
521
560
  uuid: selectedObject.uuid
522
561
  });
523
562
  }
524
563
  else {
525
- // 点击空白 清除高亮
564
+ // Click blank -> Clear highlight
526
565
  if (selectedObject)
527
566
  restoreObject();
528
567
  selectedObject = null;
529
568
  onClick(null);
530
569
  }
531
570
  }
532
- // 使用 AbortController signal 注册事件
571
+ // Register events using signal from AbortController
533
572
  renderer.domElement.addEventListener('mousedown', handleMouseDown, { signal });
534
573
  renderer.domElement.addEventListener('mouseup', handleMouseUp, { signal });
535
- /** 销毁函数:解绑事件并清除高亮 */
574
+ /** Dispose function: Unbind events and clear highlight */
536
575
  return () => {
537
- // 清理防抖定时器
576
+ // Clear debounce timer
538
577
  if (debounceTimer !== null) {
539
578
  clearTimeout(debounceTimer);
540
579
  debounceTimer = null;
541
580
  }
542
- // 一次性解绑所有事件
581
+ // Unbind all events at once
543
582
  abortController.abort();
544
- // 清除高亮
583
+ // Clear highlight
545
584
  restoreObject();
546
- // 清空引用
585
+ // Clear reference
547
586
  selectedObject = null;
548
587
  };
549
588
  }
550
589
 
551
- // src/utils/ArrowGuide.ts
552
590
  /**
553
- * ArrowGuide - 优化版
554
- * 箭头引导效果工具,支持高亮模型并淡化其他对象
591
+ * @file arrowGuide.ts
592
+ * @description
593
+ * Arrow guide effect tool, supports highlighting models and fading other objects.
594
+ *
595
+ * @best-practice
596
+ * - Use `highlight` to focus on specific models.
597
+ * - Automatically manages materials and memory using WeakMap.
598
+ * - Call `dispose` when component unmounts.
599
+ */
600
+ /**
601
+ * ArrowGuide - Optimized Version
602
+ * Arrow guide effect tool, supports highlighting models and fading other objects.
555
603
  *
556
- * ✨ 优化内容:
557
- * - 使用 WeakMap 自动回收材质,避免内存泄漏
558
- * - 使用 AbortController 管理事件生命周期
559
- * - 添加材质复用机制,减少重复创建
560
- * - 改进 dispose 逻辑,确保完全释放资源
561
- * - 添加错误处理和边界检查
604
+ * Features:
605
+ * - Uses WeakMap for automatic material recycling, preventing memory leaks
606
+ * - Uses AbortController to manage event lifecycle
607
+ * - Adds material reuse mechanism to reuse materials
608
+ * - Improved dispose logic ensuring complete resource release
609
+ * - Adds error handling and boundary checks
562
610
  */
563
611
  class ArrowGuide {
564
612
  constructor(renderer, camera, scene, options) {
@@ -573,12 +621,12 @@ class ArrowGuide {
573
621
  this.clickThreshold = 10;
574
622
  this.raycaster = new THREE.Raycaster();
575
623
  this.mouse = new THREE.Vector2();
576
- // 使用 WeakMap 自动回收材质(GC 友好)
624
+ // Use WeakMap for automatic material recycling (GC friendly)
577
625
  this.originalMaterials = new WeakMap();
578
626
  this.fadedMaterials = new WeakMap();
579
- // AbortController 用于事件管理
627
+ // AbortController for event management
580
628
  this.abortController = null;
581
- // 配置:非高亮透明度和亮度
629
+ // Config: Non-highlight opacity and brightness
582
630
  this.fadeOpacity = 0.5;
583
631
  this.fadeBrightness = 0.1;
584
632
  this.clickThreshold = (_a = options === null || options === void 0 ? void 0 : options.clickThreshold) !== null && _a !== void 0 ? _a : 10;
@@ -588,30 +636,30 @@ class ArrowGuide {
588
636
  this.abortController = new AbortController();
589
637
  this.initEvents();
590
638
  }
591
- // —— 工具:缓存原材质(仅首次)
639
+ // Tool: Cache original material (first time only)
592
640
  cacheOriginalMaterial(mesh) {
593
641
  if (!this.originalMaterials.has(mesh)) {
594
642
  this.originalMaterials.set(mesh, mesh.material);
595
643
  }
596
644
  }
597
- // —— 工具:为某个材质克隆一个"半透明版本",保留所有贴图与参数
645
+ // Tool: Clone a "translucent version" for a material, preserving all maps and parameters
598
646
  makeFadedClone(mat) {
599
647
  const clone = mat.clone();
600
648
  const c = clone;
601
- // 只改透明相关参数,不改 map / normalMap / roughnessMap 等细节
649
+ // Only modify transparency parameters, do not modify detail maps like map / normalMap / roughnessMap
602
650
  c.transparent = true;
603
651
  if (typeof c.opacity === 'number')
604
652
  c.opacity = this.fadeOpacity;
605
653
  if (c.color && c.color.isColor) {
606
- c.color.multiplyScalar(this.fadeBrightness); // 颜色整体变暗
654
+ c.color.multiplyScalar(this.fadeBrightness); // Darken color overall
607
655
  }
608
- // 为了让箭头在透明建筑后也能顺畅显示,常用策略:不写深度,仅测试深度
656
+ // Common strategy for fluid display behind transparent objects: do not write depth, only test depth
609
657
  clone.depthWrite = false;
610
658
  clone.depthTest = true;
611
659
  clone.needsUpdate = true;
612
660
  return clone;
613
661
  }
614
- // —— 工具:为 mesh.material(可能是数组)批量克隆"半透明版本"
662
+ // Tool: Batch clone "translucent version" for mesh.material (could be array)
615
663
  createFadedMaterialFrom(mesh) {
616
664
  const orig = mesh.material;
617
665
  if (Array.isArray(orig)) {
@@ -620,7 +668,7 @@ class ArrowGuide {
620
668
  return this.makeFadedClone(orig);
621
669
  }
622
670
  /**
623
- * 设置箭头 Mesh
671
+ * Set Arrow Mesh
624
672
  */
625
673
  setArrowMesh(mesh) {
626
674
  this.lxMesh = mesh;
@@ -636,15 +684,15 @@ class ArrowGuide {
636
684
  mesh.visible = false;
637
685
  }
638
686
  catch (error) {
639
- console.error('ArrowGuide: 设置箭头材质失败', error);
687
+ console.error('ArrowGuide: Failed to set arrow material', error);
640
688
  }
641
689
  }
642
690
  /**
643
- * 高亮指定模型
691
+ * Highlight specified models
644
692
  */
645
693
  highlight(models) {
646
694
  if (!models || models.length === 0) {
647
- console.warn('ArrowGuide: 高亮模型列表为空');
695
+ console.warn('ArrowGuide: Highlight model list is empty');
648
696
  return;
649
697
  }
650
698
  this.modelBrightArr = models;
@@ -653,9 +701,9 @@ class ArrowGuide {
653
701
  this.lxMesh.visible = true;
654
702
  this.applyHighlight();
655
703
  }
656
- // 应用高亮效果:非高亮模型保留细节 使用"克隆后的半透明材质"
704
+ // Apply highlight effect: Non-highlighted models preserve details -> use "cloned translucent material"
657
705
  applyHighlight() {
658
- // 使用 Set 提升查找性能
706
+ // Use Set to improve lookup performance
659
707
  const keepMeshes = new Set();
660
708
  this.modelBrightArr.forEach(obj => {
661
709
  obj.traverse(child => {
@@ -667,21 +715,21 @@ class ArrowGuide {
667
715
  this.scene.traverse(obj => {
668
716
  if (obj.isMesh) {
669
717
  const mesh = obj;
670
- // 缓存原材质(用于恢复)
718
+ // Cache original material (for restoration)
671
719
  this.cacheOriginalMaterial(mesh);
672
720
  if (!keepMeshes.has(mesh)) {
673
- // 非高亮:如果还没给它生成过"半透明克隆材质",就创建一次
721
+ // Non-highlighted: if no "translucent clone material" generated yet, create one
674
722
  if (!this.fadedMaterials.has(mesh)) {
675
723
  const faded = this.createFadedMaterialFrom(mesh);
676
724
  this.fadedMaterials.set(mesh, faded);
677
725
  }
678
- // 替换为克隆材质(保留所有贴图/法线等细节)
726
+ // Replace with clone material (preserve all maps/normals details)
679
727
  const fadedMat = this.fadedMaterials.get(mesh);
680
728
  if (fadedMat)
681
729
  mesh.material = fadedMat;
682
730
  }
683
731
  else {
684
- // 高亮对象:确保回到原材质(避免上一次高亮后遗留)
732
+ // Highlighted object: ensure return to original material (avoid leftover from previous highlight)
685
733
  const orig = this.originalMaterials.get(mesh);
686
734
  if (orig && mesh.material !== orig) {
687
735
  mesh.material = orig;
@@ -692,16 +740,16 @@ class ArrowGuide {
692
740
  });
693
741
  }
694
742
  catch (error) {
695
- console.error('ArrowGuide: 应用高亮失败', error);
743
+ console.error('ArrowGuide: Failed to apply highlight', error);
696
744
  }
697
745
  }
698
- // 恢复为原材质 & 释放克隆材质
746
+ // Restore to original material & dispose clone material
699
747
  restore() {
700
748
  this.flowActive = false;
701
749
  if (this.lxMesh)
702
750
  this.lxMesh.visible = false;
703
751
  try {
704
- // 收集所有需要释放的材质
752
+ // Collect all materials to dispose
705
753
  const materialsToDispose = [];
706
754
  this.scene.traverse(obj => {
707
755
  if (obj.isMesh) {
@@ -711,7 +759,7 @@ class ArrowGuide {
711
759
  mesh.material = orig;
712
760
  mesh.material.needsUpdate = true;
713
761
  }
714
- // 收集待释放的淡化材质
762
+ // Collect faded materials to dispose
715
763
  const faded = this.fadedMaterials.get(mesh);
716
764
  if (faded) {
717
765
  if (Array.isArray(faded)) {
@@ -723,24 +771,24 @@ class ArrowGuide {
723
771
  }
724
772
  }
725
773
  });
726
- // 批量释放材质(不触碰贴图资源)
774
+ // Batch dispose materials (do not touch texture resources)
727
775
  materialsToDispose.forEach(mat => {
728
776
  try {
729
777
  mat.dispose();
730
778
  }
731
779
  catch (error) {
732
- console.error('ArrowGuide: 释放材质失败', error);
780
+ console.error('ArrowGuide: Failed to dispose material', error);
733
781
  }
734
782
  });
735
- // 创建新的 WeakMap(相当于清空)
783
+ // Create new WeakMap (equivalent to clearing)
736
784
  this.fadedMaterials = new WeakMap();
737
785
  }
738
786
  catch (error) {
739
- console.error('ArrowGuide: 恢复材质失败', error);
787
+ console.error('ArrowGuide: Failed to restore material', error);
740
788
  }
741
789
  }
742
790
  /**
743
- * 动画更新(每帧调用)
791
+ * Animation update (called every frame)
744
792
  */
745
793
  animate() {
746
794
  if (!this.flowActive || !this.lxMesh)
@@ -754,16 +802,16 @@ class ArrowGuide {
754
802
  }
755
803
  }
756
804
  catch (error) {
757
- console.error('ArrowGuide: 动画更新失败', error);
805
+ console.error('ArrowGuide: Animation update failed', error);
758
806
  }
759
807
  }
760
808
  /**
761
- * 初始化事件监听器
809
+ * Initialize event listeners
762
810
  */
763
811
  initEvents() {
764
812
  const dom = this.renderer.domElement;
765
813
  const signal = this.abortController.signal;
766
- // 使用 AbortController signal 自动管理事件生命周期
814
+ // Use AbortController signal to automatically manage event lifecycle
767
815
  dom.addEventListener('pointerdown', (e) => {
768
816
  this.pointerDownPos.set(e.clientX, e.clientY);
769
817
  }, { signal });
@@ -771,7 +819,7 @@ class ArrowGuide {
771
819
  const dx = Math.abs(e.clientX - this.pointerDownPos.x);
772
820
  const dy = Math.abs(e.clientY - this.pointerDownPos.y);
773
821
  if (dx > this.clickThreshold || dy > this.clickThreshold)
774
- return; // 拖拽
822
+ return; // Dragging
775
823
  const rect = dom.getBoundingClientRect();
776
824
  this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
777
825
  this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
@@ -785,21 +833,21 @@ class ArrowGuide {
785
833
  return true;
786
834
  });
787
835
  if (filtered.length === 0)
788
- this.restore(); // 点击空白恢复
836
+ this.restore(); // Click blank space to restore
789
837
  }, { signal });
790
838
  }
791
839
  /**
792
- * 释放所有资源
840
+ * Dispose all resources
793
841
  */
794
842
  dispose() {
795
- // 先恢复材质
843
+ // Restore materials first
796
844
  this.restore();
797
- // 使用 AbortController 一次性解绑所有事件
845
+ // Unbind all events at once using AbortController
798
846
  if (this.abortController) {
799
847
  this.abortController.abort();
800
848
  this.abortController = null;
801
849
  }
802
- // 清空引用
850
+ // Clear references
803
851
  this.modelBrightArr = [];
804
852
  this.lxMesh = null;
805
853
  this.fadedMaterials = new WeakMap();
@@ -808,50 +856,59 @@ class ArrowGuide {
808
856
  }
809
857
  }
810
858
 
811
- // utils/LiquidFillerGroup.ts
812
859
  /**
813
- * LiquidFillerGroup - 优化版
814
- * 支持单模型或多模型液位动画、独立颜色控制
860
+ * @file liquidFiller.ts
861
+ * @description
862
+ * Liquid filling effect for single or multiple models using local clipping planes.
815
863
  *
816
- * ✨ 优化内容:
817
- * - 使用 renderer.domElement 替代 window 事件
818
- * - 使用 AbortController 管理事件生命周期
819
- * - 添加错误处理和边界检查
820
- * - 优化动画管理,避免内存泄漏
821
- * - 完善资源释放逻辑
864
+ * @best-practice
865
+ * - Use `fillTo` to animate liquid level.
866
+ * - Supports multiple independent liquid levels.
867
+ * - Call `dispose` to clean up resources and event listeners.
868
+ */
869
+ /**
870
+ * LiquidFillerGroup - Optimized
871
+ * Supports single or multi-model liquid level animation with independent color control.
872
+ *
873
+ * Features:
874
+ * - Uses renderer.domElement instead of window events
875
+ * - Uses AbortController to manage event lifecycle
876
+ * - Adds error handling and boundary checks
877
+ * - Optimized animation management to prevent memory leaks
878
+ * - Comprehensive resource disposal logic
822
879
  */
823
880
  class LiquidFillerGroup {
824
881
  /**
825
- * 构造函数
826
- * @param models 单个或多个 THREE.Object3D
827
- * @param scene 场景
828
- * @param camera 相机
829
- * @param renderer 渲染器
830
- * @param defaultOptions 默认液体选项
831
- * @param clickThreshold 点击阈值,单位像素
882
+ * Constructor
883
+ * @param models Single or multiple THREE.Object3D
884
+ * @param scene Scene
885
+ * @param camera Camera
886
+ * @param renderer Renderer
887
+ * @param defaultOptions Default liquid options
888
+ * @param clickThreshold Click threshold in pixels
832
889
  */
833
890
  constructor(models, scene, camera, renderer, defaultOptions, clickThreshold = 10) {
834
891
  this.items = [];
835
892
  this.raycaster = new THREE.Raycaster();
836
893
  this.pointerDownPos = new THREE.Vector2();
837
894
  this.clickThreshold = 10;
838
- this.abortController = null; // 事件管理器
839
- /** pointerdown 记录位置 */
895
+ this.abortController = null; // Event manager
896
+ /** pointerdown record position */
840
897
  this.handlePointerDown = (event) => {
841
898
  this.pointerDownPos.set(event.clientX, event.clientY);
842
899
  };
843
- /** pointerup 判断点击空白,恢复原始材质 */
900
+ /** pointerup check click blank, restore original material */
844
901
  this.handlePointerUp = (event) => {
845
902
  const dx = event.clientX - this.pointerDownPos.x;
846
903
  const dy = event.clientY - this.pointerDownPos.y;
847
904
  const distance = Math.sqrt(dx * dx + dy * dy);
848
905
  if (distance > this.clickThreshold)
849
- return; // 拖拽不触发
850
- // 使用 renderer.domElement 的实际尺寸
906
+ return; // Do not trigger on drag
907
+ // Use renderer.domElement actual size
851
908
  const rect = this.renderer.domElement.getBoundingClientRect();
852
909
  const pointerNDC = new THREE.Vector2(((event.clientX - rect.left) / rect.width) * 2 - 1, -((event.clientY - rect.top) / rect.height) * 2 + 1);
853
910
  this.raycaster.setFromCamera(pointerNDC, this.camera);
854
- // 点击空白 -> 所有模型恢复
911
+ // Click blank -> Restore all models
855
912
  const intersectsAny = this.items.some(item => this.raycaster.intersectObject(item.model, true).length > 0);
856
913
  if (!intersectsAny) {
857
914
  this.restoreAll();
@@ -861,7 +918,7 @@ class LiquidFillerGroup {
861
918
  this.camera = camera;
862
919
  this.renderer = renderer;
863
920
  this.clickThreshold = clickThreshold;
864
- // 创建 AbortController 用于事件管理
921
+ // Create AbortController for event management
865
922
  this.abortController = new AbortController();
866
923
  const modelArray = Array.isArray(models) ? models : [models];
867
924
  modelArray.forEach(model => {
@@ -872,7 +929,7 @@ class LiquidFillerGroup {
872
929
  opacity: (_b = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.opacity) !== null && _b !== void 0 ? _b : 0.6,
873
930
  speed: (_c = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.speed) !== null && _c !== void 0 ? _c : 0.05,
874
931
  };
875
- // 保存原始材质
932
+ // Save original materials
876
933
  const originalMaterials = new Map();
877
934
  model.traverse(obj => {
878
935
  if (obj.isMesh) {
@@ -880,12 +937,12 @@ class LiquidFillerGroup {
880
937
  originalMaterials.set(mesh, mesh.material);
881
938
  }
882
939
  });
883
- // 边界检查:确保有材质可以保存
940
+ // Boundary check: ensure there are materials to save
884
941
  if (originalMaterials.size === 0) {
885
- console.warn('LiquidFillerGroup: 模型没有 Mesh 对象', model);
942
+ console.warn('LiquidFillerGroup: Model has no Mesh objects', model);
886
943
  return;
887
944
  }
888
- // 应用淡线框材质
945
+ // Apply faded wireframe material
889
946
  model.traverse(obj => {
890
947
  if (obj.isMesh) {
891
948
  const mesh = obj;
@@ -897,7 +954,7 @@ class LiquidFillerGroup {
897
954
  });
898
955
  }
899
956
  });
900
- // 创建液体 Mesh
957
+ // Create liquid Mesh
901
958
  const geometries = [];
902
959
  model.traverse(obj => {
903
960
  if (obj.isMesh) {
@@ -908,12 +965,12 @@ class LiquidFillerGroup {
908
965
  }
909
966
  });
910
967
  if (geometries.length === 0) {
911
- console.warn('LiquidFillerGroup: 模型没有几何体', model);
968
+ console.warn('LiquidFillerGroup: Model has no geometries', model);
912
969
  return;
913
970
  }
914
971
  const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries, false);
915
972
  if (!mergedGeometry) {
916
- console.error('LiquidFillerGroup: 几何体合并失败', model);
973
+ console.error('LiquidFillerGroup: Failed to merge geometries', model);
917
974
  return;
918
975
  }
919
976
  const material = new THREE.MeshPhongMaterial({
@@ -924,7 +981,7 @@ class LiquidFillerGroup {
924
981
  });
925
982
  const liquidMesh = new THREE.Mesh(mergedGeometry, material);
926
983
  this.scene.add(liquidMesh);
927
- // 设置 clippingPlane
984
+ // Set clippingPlane
928
985
  const clipPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0);
929
986
  const mat = liquidMesh.material;
930
987
  mat.clippingPlanes = [clipPlane];
@@ -935,41 +992,41 @@ class LiquidFillerGroup {
935
992
  clipPlane,
936
993
  originalMaterials,
937
994
  options,
938
- animationId: null // 初始化动画 ID
995
+ animationId: null // Initialize animation ID
939
996
  });
940
997
  }
941
998
  catch (error) {
942
- console.error('LiquidFillerGroup: 初始化模型失败', model, error);
999
+ console.error('LiquidFillerGroup: Failed to initialize model', model, error);
943
1000
  }
944
1001
  });
945
- // 使用 renderer.domElement 替代 window,使用 AbortController signal
1002
+ // Use renderer.domElement instead of window, use AbortController signal
946
1003
  const signal = this.abortController.signal;
947
1004
  this.renderer.domElement.addEventListener('pointerdown', this.handlePointerDown, { signal });
948
1005
  this.renderer.domElement.addEventListener('pointerup', this.handlePointerUp, { signal });
949
1006
  }
950
1007
  /**
951
- * 设置液位
952
- * @param models 单个模型或模型数组
953
- * @param percent 液位百分比 0~1
954
- */
1008
+ * Set liquid level
1009
+ * @param models Single model or array of models
1010
+ * @param percent Liquid level percentage 0~1
1011
+ */
955
1012
  fillTo(models, percent) {
956
- // 边界检查
1013
+ // Boundary check
957
1014
  if (percent < 0 || percent > 1) {
958
- console.warn('LiquidFillerGroup: percent 必须在 0~1 之间', percent);
1015
+ console.warn('LiquidFillerGroup: percent must be between 0 and 1', percent);
959
1016
  percent = Math.max(0, Math.min(1, percent));
960
1017
  }
961
1018
  const modelArray = Array.isArray(models) ? models : [models];
962
1019
  modelArray.forEach(model => {
963
1020
  const item = this.items.find(i => i.model === model);
964
1021
  if (!item) {
965
- console.warn('LiquidFillerGroup: 未找到模型', model);
1022
+ console.warn('LiquidFillerGroup: Model not found', model);
966
1023
  return;
967
1024
  }
968
1025
  if (!item.liquidMesh) {
969
- console.warn('LiquidFillerGroup: liquidMesh 已被释放', model);
1026
+ console.warn('LiquidFillerGroup: liquidMesh already disposed', model);
970
1027
  return;
971
1028
  }
972
- // 取消之前的动画
1029
+ // Cancel previous animation
973
1030
  if (item.animationId !== null) {
974
1031
  cancelAnimationFrame(item.animationId);
975
1032
  item.animationId = null;
@@ -997,14 +1054,14 @@ class LiquidFillerGroup {
997
1054
  animate();
998
1055
  }
999
1056
  catch (error) {
1000
- console.error('LiquidFillerGroup: fillTo 执行失败', model, error);
1057
+ console.error('LiquidFillerGroup: fillTo execution failed', model, error);
1001
1058
  }
1002
1059
  });
1003
1060
  }
1004
- /** 设置多个模型液位,percentList items 顺序对应 */
1061
+ /** Set multiple model levels, percentList corresponds to items order */
1005
1062
  fillToAll(percentList) {
1006
1063
  if (percentList.length !== this.items.length) {
1007
- console.warn(`LiquidFillerGroup: percentList 长度 (${percentList.length}) items 长度 (${this.items.length}) 不匹配`);
1064
+ console.warn(`LiquidFillerGroup: percentList length (${percentList.length}) does not match items length (${this.items.length})`);
1008
1065
  }
1009
1066
  percentList.forEach((p, idx) => {
1010
1067
  if (idx < this.items.length) {
@@ -1012,17 +1069,17 @@ class LiquidFillerGroup {
1012
1069
  }
1013
1070
  });
1014
1071
  }
1015
- /** 恢复单个模型原始材质并移除液体 */
1072
+ /** Restore single model original material and remove liquid */
1016
1073
  restore(model) {
1017
1074
  const item = this.items.find(i => i.model === model);
1018
1075
  if (!item)
1019
1076
  return;
1020
- // 取消动画
1077
+ // Cancel animation
1021
1078
  if (item.animationId !== null) {
1022
1079
  cancelAnimationFrame(item.animationId);
1023
1080
  item.animationId = null;
1024
1081
  }
1025
- // 恢复原始材质
1082
+ // Restore original material
1026
1083
  item.model.traverse(obj => {
1027
1084
  if (obj.isMesh) {
1028
1085
  const mesh = obj;
@@ -1031,7 +1088,7 @@ class LiquidFillerGroup {
1031
1088
  mesh.material = original;
1032
1089
  }
1033
1090
  });
1034
- // 释放液体 Mesh
1091
+ // Dispose liquid Mesh
1035
1092
  if (item.liquidMesh) {
1036
1093
  this.scene.remove(item.liquidMesh);
1037
1094
  item.liquidMesh.geometry.dispose();
@@ -1044,50 +1101,60 @@ class LiquidFillerGroup {
1044
1101
  item.liquidMesh = null;
1045
1102
  }
1046
1103
  }
1047
- /** 恢复所有模型 */
1104
+ /** Restore all models */
1048
1105
  restoreAll() {
1049
1106
  this.items.forEach(item => this.restore(item.model));
1050
1107
  }
1051
- /** 销毁方法,释放事件和资源 */
1108
+ /** Dispose method, release events and resources */
1052
1109
  dispose() {
1053
- // 先恢复所有模型
1110
+ // Restore all models first
1054
1111
  this.restoreAll();
1055
- // 使用 AbortController 一次性解绑所有事件
1112
+ // Unbind all events at once using AbortController
1056
1113
  if (this.abortController) {
1057
1114
  this.abortController.abort();
1058
1115
  this.abortController = null;
1059
1116
  }
1060
- // 清空 items
1117
+ // Clear items
1061
1118
  this.items.length = 0;
1062
1119
  }
1063
1120
  }
1064
1121
 
1065
- // src/utils/followModels.ts - 优化版
1066
- // 使用 WeakMap 跟踪动画,支持取消
1122
+ /**
1123
+ * @file followModels.ts
1124
+ * @description
1125
+ * Camera utility to automatically follow and focus on 3D models.
1126
+ * It smoothly moves the camera to an optimal viewing position relative to the target object(s).
1127
+ *
1128
+ * @best-practice
1129
+ * - Use `followModels` to focus on a newly selected object.
1130
+ * - Call `cancelFollow` before starting a new manual camera interaction if needed.
1131
+ * - Adjust `padding` to control how tight the camera framing is.
1132
+ */
1133
+ // Use WeakMap to track animations, allowing for cancellation
1067
1134
  const _animationMap = new WeakMap();
1068
1135
  /**
1069
- * 推荐角度枚举,便于快速选取常见视角
1136
+ * Recommended camera angles for quick selection of common views
1070
1137
  */
1071
1138
  const FOLLOW_ANGLES = {
1072
- /** 等距斜视(默认视角)- 适合建筑、机械设备展示 */
1139
+ /** Isometric view (default) - suitable for architecture, mechanical equipment */
1073
1140
  ISOMETRIC: { azimuth: Math.PI / 4, elevation: Math.PI / 4 },
1074
- /** 正前视角 - 适合正面展示、UI 对齐 */
1141
+ /** Front view - suitable for frontal display, UI alignment */
1075
1142
  FRONT: { azimuth: 0, elevation: 0 },
1076
- /** 右侧视角 - 适合机械剖面、侧视检查 */
1143
+ /** Right view - suitable for mechanical sections, side inspection */
1077
1144
  RIGHT: { azimuth: Math.PI / 2, elevation: 0 },
1078
- /** 左侧视角 */
1145
+ /** Left view */
1079
1146
  LEFT: { azimuth: -Math.PI / 2, elevation: 0 },
1080
- /** 后视角 */
1147
+ /** Back view */
1081
1148
  BACK: { azimuth: Math.PI, elevation: 0 },
1082
- /** 顶视图 - 适合地图、平面布局展示 */
1149
+ /** Top view - suitable for maps, layout display */
1083
1150
  TOP: { azimuth: 0, elevation: Math.PI / 2 },
1084
- /** 低角度俯视 - 适合车辆、人物等近地物体 */
1151
+ /** Low angle view - suitable for vehicles, characters near the ground */
1085
1152
  LOW_ANGLE: { azimuth: Math.PI / 4, elevation: Math.PI / 6 },
1086
- /** 高角度俯视 - 适合鸟瞰、全景浏览 */
1153
+ /** High angle view - suitable for bird's eye view, panoramic browsing */
1087
1154
  HIGH_ANGLE: { azimuth: Math.PI / 4, elevation: Math.PI / 3 }
1088
1155
  };
1089
1156
  /**
1090
- * 缓动函数集合
1157
+ * Collection of easing functions
1091
1158
  */
1092
1159
  const EASING_FUNCTIONS = {
1093
1160
  linear: (t) => t,
@@ -1096,20 +1163,21 @@ const EASING_FUNCTIONS = {
1096
1163
  easeIn: (t) => t * t * t
1097
1164
  };
1098
1165
  /**
1099
- * 自动将相机移到目标的斜上角位置,并保证目标在可视范围内(平滑过渡)- 优化版
1166
+ * Automatically moves the camera to a diagonal position relative to the target,
1167
+ * ensuring the target is within the field of view (smooth transition).
1100
1168
  *
1101
- * ✨ 优化内容:
1102
- * - 支持多种缓动函数
1103
- * - 添加进度回调
1104
- * - 支持取消动画
1105
- * - WeakMap 跟踪防止泄漏
1106
- * - 完善错误处理
1169
+ * Features:
1170
+ * - Supports multiple easing functions
1171
+ * - Adds progress callback
1172
+ * - Supports animation cancellation
1173
+ * - Uses WeakMap to track and prevent memory leaks
1174
+ * - Robust error handling
1107
1175
  */
1108
1176
  function followModels(camera, targets, options = {}) {
1109
1177
  var _a, _b, _c, _d, _e, _f;
1110
- // 取消之前的动画
1178
+ // Cancel previous animation
1111
1179
  cancelFollow(camera);
1112
- // 边界检查
1180
+ // Boundary check
1113
1181
  const arr = [];
1114
1182
  if (!targets)
1115
1183
  return Promise.resolve();
@@ -1118,15 +1186,15 @@ function followModels(camera, targets, options = {}) {
1118
1186
  else
1119
1187
  arr.push(targets);
1120
1188
  if (arr.length === 0) {
1121
- console.warn('followModels: 目标对象为空');
1189
+ console.warn('followModels: Target object is empty');
1122
1190
  return Promise.resolve();
1123
1191
  }
1124
1192
  try {
1125
1193
  const box = new THREE.Box3();
1126
1194
  arr.forEach((o) => box.expandByObject(o));
1127
- // 检查包围盒有效性
1195
+ // Check bounding box validity
1128
1196
  if (!isFinite(box.min.x) || !isFinite(box.max.x)) {
1129
- console.warn('followModels: 包围盒计算失败');
1197
+ console.warn('followModels: Failed to calculate bounding box');
1130
1198
  return Promise.resolve();
1131
1199
  }
1132
1200
  const sphere = new THREE.Sphere();
@@ -1142,7 +1210,7 @@ function followModels(camera, targets, options = {}) {
1142
1210
  const elevation = (_e = options.elevation) !== null && _e !== void 0 ? _e : Math.PI / 4;
1143
1211
  const easing = (_f = options.easing) !== null && _f !== void 0 ? _f : 'easeOut';
1144
1212
  const onProgress = options.onProgress;
1145
- // 获取缓动函数
1213
+ // Get easing function
1146
1214
  const easingFn = EASING_FUNCTIONS[easing] || EASING_FUNCTIONS.easeOut;
1147
1215
  let distance = 10;
1148
1216
  if (camera.isPerspectiveCamera) {
@@ -1162,7 +1230,7 @@ function followModels(camera, targets, options = {}) {
1162
1230
  else {
1163
1231
  distance = camera.position.distanceTo(center);
1164
1232
  }
1165
- // 根据 azimuth / elevation 计算方向
1233
+ // Calculate direction based on azimuth / elevation
1166
1234
  const hx = Math.sin(azimuth);
1167
1235
  const hz = Math.cos(azimuth);
1168
1236
  const dir = new THREE.Vector3(hx * Math.cos(elevation), Math.sin(elevation), hz * Math.cos(elevation)).normalize();
@@ -1175,7 +1243,6 @@ function followModels(camera, targets, options = {}) {
1175
1243
  const startTime = performance.now();
1176
1244
  return new Promise((resolve) => {
1177
1245
  const step = (now) => {
1178
- var _a;
1179
1246
  const elapsed = now - startTime;
1180
1247
  const t = Math.min(1, duration > 0 ? elapsed / duration : 1);
1181
1248
  const k = easingFn(t);
@@ -1189,14 +1256,16 @@ function followModels(camera, targets, options = {}) {
1189
1256
  else {
1190
1257
  camera.lookAt(endTarget);
1191
1258
  }
1192
- (_a = camera.updateProjectionMatrix) === null || _a === void 0 ? void 0 : _a.call(camera);
1193
- // ✨ 调用进度回调
1259
+ if (camera.updateProjectionMatrix) {
1260
+ camera.updateProjectionMatrix();
1261
+ }
1262
+ // Call progress callback
1194
1263
  if (onProgress) {
1195
1264
  try {
1196
1265
  onProgress(t);
1197
1266
  }
1198
1267
  catch (error) {
1199
- console.error('followModels: 进度回调错误', error);
1268
+ console.error('followModels: Progress callback error', error);
1200
1269
  }
1201
1270
  }
1202
1271
  if (t < 1) {
@@ -1222,12 +1291,12 @@ function followModels(camera, targets, options = {}) {
1222
1291
  });
1223
1292
  }
1224
1293
  catch (error) {
1225
- console.error('followModels: 执行失败', error);
1294
+ console.error('followModels: Execution failed', error);
1226
1295
  return Promise.reject(error);
1227
1296
  }
1228
1297
  }
1229
1298
  /**
1230
- * 取消相机的跟随动画
1299
+ * Cancel the camera follow animation
1231
1300
  */
1232
1301
  function cancelFollow(camera) {
1233
1302
  const rafId = _animationMap.get(camera);
@@ -1237,42 +1306,50 @@ function cancelFollow(camera) {
1237
1306
  }
1238
1307
  }
1239
1308
 
1240
- // src/utils/setView.ts - 优化版
1241
1309
  /**
1242
- * 平滑切换相机到模型的最佳视角 - 优化版
1310
+ * @file setView.ts
1311
+ * @description
1312
+ * Utility to smoothly transition the camera to preset views (Front, Back, Top, Isometric, etc.).
1313
+ *
1314
+ * @best-practice
1315
+ * - Use `setView` for UI buttons that switch camera angles.
1316
+ * - Leverage `ViewPresets` for readable code when using standard views.
1317
+ */
1318
+ /**
1319
+ * Smoothly switches the camera to the optimal angle for the model.
1243
1320
  *
1244
- * ✨ 优化内容:
1245
- * - 复用 followModels 逻辑,避免代码重复
1246
- * - 支持更多视角
1247
- * - 配置选项增强
1248
- * - 返回 Promise 支持链式调用
1249
- * - 支持取消动画
1321
+ * Features:
1322
+ * - Reuses followModels logic to avoid code duplication
1323
+ * - Supports more angles
1324
+ * - Enhanced configuration options
1325
+ * - Returns Promise to support chaining
1326
+ * - Supports animation cancellation
1250
1327
  *
1251
- * @param camera THREE.PerspectiveCamera 相机实例
1252
- * @param controls OrbitControls 控制器实例
1253
- * @param targetObj THREE.Object3D 模型对象
1254
- * @param position 视角位置
1255
- * @param options 配置选项
1328
+ * @param camera THREE.PerspectiveCamera instance
1329
+ * @param controls OrbitControls instance
1330
+ * @param targetObj THREE.Object3D model object
1331
+ * @param position View position
1332
+ * @param options Configuration options
1256
1333
  * @returns Promise<void>
1257
1334
  */
1258
1335
  function setView(camera, controls, targetObj, position = 'front', options = {}) {
1259
1336
  const { distanceFactor = 0.8, duration = 1000, easing = 'easeInOut', onProgress } = options;
1260
- // 边界检查
1337
+ // Boundary check
1261
1338
  if (!targetObj) {
1262
- console.warn('setView: 目标对象为空');
1339
+ console.warn('setView: Target object is empty');
1263
1340
  return Promise.reject(new Error('Target object is required'));
1264
1341
  }
1265
1342
  try {
1266
- // 计算包围盒
1343
+ // Calculate bounding box
1267
1344
  const box = new THREE.Box3().setFromObject(targetObj);
1268
1345
  if (!isFinite(box.min.x)) {
1269
- console.warn('setView: 包围盒计算失败');
1346
+ console.warn('setView: Failed to calculate bounding box');
1270
1347
  return Promise.reject(new Error('Invalid bounding box'));
1271
1348
  }
1272
1349
  const center = box.getCenter(new THREE.Vector3());
1273
1350
  const size = box.getSize(new THREE.Vector3());
1274
1351
  const maxSize = Math.max(size.x, size.y, size.z);
1275
- // 使用映射表简化视角计算
1352
+ // Use mapping table for creating view angles
1276
1353
  const viewAngles = {
1277
1354
  'front': { azimuth: 0, elevation: 0 },
1278
1355
  'back': { azimuth: Math.PI, elevation: 0 },
@@ -1283,7 +1360,7 @@ function setView(camera, controls, targetObj, position = 'front', options = {})
1283
1360
  'iso': { azimuth: Math.PI / 4, elevation: Math.PI / 4 }
1284
1361
  };
1285
1362
  const angle = viewAngles[position] || viewAngles.front;
1286
- // 复用 followModels,避免代码重复
1363
+ // Reuse followModels to avoid code duplication
1287
1364
  return followModels(camera, targetObj, {
1288
1365
  duration,
1289
1366
  padding: distanceFactor,
@@ -1295,30 +1372,30 @@ function setView(camera, controls, targetObj, position = 'front', options = {})
1295
1372
  });
1296
1373
  }
1297
1374
  catch (error) {
1298
- console.error('setView: 执行失败', error);
1375
+ console.error('setView: Execution failed', error);
1299
1376
  return Promise.reject(error);
1300
1377
  }
1301
1378
  }
1302
1379
  /**
1303
- * 取消视角切换动画
1380
+ * Cancel view switch animation
1304
1381
  */
1305
1382
  function cancelSetView(camera) {
1306
1383
  cancelFollow(camera);
1307
1384
  }
1308
1385
  /**
1309
- * 预设视角快捷方法
1386
+ * Preset view shortcut methods
1310
1387
  */
1311
1388
  const ViewPresets = {
1312
1389
  /**
1313
- * 前视图
1390
+ * Front View
1314
1391
  */
1315
1392
  front: (camera, controls, target, options) => setView(camera, controls, target, 'front', options),
1316
1393
  /**
1317
- * 等距视图
1394
+ * Isometric View
1318
1395
  */
1319
1396
  isometric: (camera, controls, target, options) => setView(camera, controls, target, 'iso', options),
1320
1397
  /**
1321
- * 顶视图
1398
+ * Top View
1322
1399
  */
1323
1400
  top: (camera, controls, target, options) => setView(camera, controls, target, 'top', options)
1324
1401
  };
@@ -1355,6 +1432,16 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
1355
1432
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
1356
1433
  };
1357
1434
 
1435
+ /**
1436
+ * @file modelLoader.ts
1437
+ * @description
1438
+ * Utility to load 3D models (GLTF, FBX, OBJ, PLY, STL) from URLs.
1439
+ *
1440
+ * @best-practice
1441
+ * - Use `loadModelByUrl` for a unified loading interface.
1442
+ * - Supports Draco compression and KTX2 textures for GLTF.
1443
+ * - Includes optimization options like geometry merging and texture downscaling.
1444
+ */
1358
1445
  const DEFAULT_OPTIONS$1 = {
1359
1446
  useKTX2: false,
1360
1447
  mergeGeometries: false,
@@ -1362,12 +1449,12 @@ const DEFAULT_OPTIONS$1 = {
1362
1449
  useSimpleMaterials: false,
1363
1450
  skipSkinned: true,
1364
1451
  };
1365
- /** 自动根据扩展名决定启用哪些选项(智能判断) */
1452
+ /** Automatically determine which options to enable based on extension (smart judgment) */
1366
1453
  function normalizeOptions(url, opts) {
1367
1454
  const ext = (url.split('.').pop() || '').toLowerCase();
1368
1455
  const merged = Object.assign(Object.assign({}, DEFAULT_OPTIONS$1), opts);
1369
1456
  if (ext === 'gltf' || ext === 'glb') {
1370
- // gltf/glb 默认尝试 draco/ktx2,如果用户没填
1457
+ // gltf/glb defaults to trying draco/ktx2 if user didn't specify
1371
1458
  if (merged.dracoDecoderPath === undefined)
1372
1459
  merged.dracoDecoderPath = '/draco/';
1373
1460
  if (merged.useKTX2 === undefined)
@@ -1376,7 +1463,7 @@ function normalizeOptions(url, opts) {
1376
1463
  merged.ktx2TranscoderPath = '/basis/';
1377
1464
  }
1378
1465
  else {
1379
- // fbx/obj/ply/stl 等不需要 draco/ktx2
1466
+ // fbx/obj/ply/stl etc. do not need draco/ktx2
1380
1467
  merged.dracoDecoderPath = null;
1381
1468
  merged.ktx2TranscoderPath = null;
1382
1469
  merged.useKTX2 = false;
@@ -1432,8 +1519,8 @@ function loadModelByUrl(url_1) {
1432
1519
  var _a;
1433
1520
  if (ext === 'gltf' || ext === 'glb') {
1434
1521
  const sceneObj = res.scene || res;
1435
- // --- 关键:把 animations 暴露到 scene.userData(或 scene.animations)上 ---
1436
- // 这样调用方只要拿到 sceneObj,就能通过 sceneObj.userData.animations 读取到 clips
1522
+ // --- Critical: Expose animations to scene.userData (or scene.animations) ---
1523
+ // So the caller can access clips simply by getting sceneObj.userData.animations
1437
1524
  sceneObj.userData = (sceneObj === null || sceneObj === void 0 ? void 0 : sceneObj.userData) || {};
1438
1525
  sceneObj.userData.animations = (_a = res.animations) !== null && _a !== void 0 ? _a : [];
1439
1526
  resolve(sceneObj);
@@ -1443,7 +1530,7 @@ function loadModelByUrl(url_1) {
1443
1530
  }
1444
1531
  }, undefined, (err) => reject(err));
1445
1532
  });
1446
- // 优化
1533
+ // Optimize
1447
1534
  object.traverse((child) => {
1448
1535
  var _a, _b, _c;
1449
1536
  const mesh = child;
@@ -1478,7 +1565,7 @@ function loadModelByUrl(url_1) {
1478
1565
  return object;
1479
1566
  });
1480
1567
  }
1481
- /** 运行时下采样网格中的贴图到 maxSizecanvas drawImage)以节省 GPU 内存 */
1568
+ /** Runtime downscale textures in mesh to maxSize (canvas drawImage) to save GPU memory */
1482
1569
  function downscaleTexturesInObject(obj, maxSize) {
1483
1570
  obj.traverse((ch) => {
1484
1571
  if (!ch.isMesh)
@@ -1521,10 +1608,10 @@ function downscaleTexturesInObject(obj, maxSize) {
1521
1608
  });
1522
1609
  }
1523
1610
  /**
1524
- * 尝试合并 object 中的几何体(只合并:非透明、非 SkinnedMeshattribute 集合兼容的 BufferGeometry
1525
- * - 合并前会把每个 mesh 的几何体应用 world matrixso merged geometry in world space
1526
- * - 合并会按材质 UUID 分组(不同材质不能合并)
1527
- * - 合并函数会兼容 BufferGeometryUtils 的常见导出名
1611
+ * Try to merge geometries in object (Only merge: non-transparent, non-SkinnedMesh, attribute compatible BufferGeometry)
1612
+ * - Before merging, apply world matrix to each mesh's geometry (so merged geometry is in world space)
1613
+ * - Merging will group by material UUID (different materials cannot be merged)
1614
+ * - Merge function is compatible with common export names of BufferGeometryUtils
1528
1615
  */
1529
1616
  function tryMergeGeometries(root, opts) {
1530
1617
  return __awaiter(this, void 0, void 0, function* () {
@@ -1604,9 +1691,9 @@ function tryMergeGeometries(root, opts) {
1604
1691
  });
1605
1692
  }
1606
1693
  /* ---------------------
1607
- 释放工具
1694
+ Dispose Utils
1608
1695
  --------------------- */
1609
- /** 彻底释放对象:几何体,材质和其贴图(危险:共享资源会被释放) */
1696
+ /** Completely dispose object: geometry, material and its textures (Danger: shared resources will be disposed) */
1610
1697
  function disposeObject(obj) {
1611
1698
  if (!obj)
1612
1699
  return;
@@ -1629,7 +1716,7 @@ function disposeObject(obj) {
1629
1716
  }
1630
1717
  });
1631
1718
  }
1632
- /** 释放材质及其贴图 */
1719
+ /** Dispose material and its textures */
1633
1720
  function disposeMaterial(mat) {
1634
1721
  if (!mat)
1635
1722
  return;
@@ -1648,25 +1735,45 @@ function disposeMaterial(mat) {
1648
1735
  }
1649
1736
  catch (_a) { }
1650
1737
  }
1738
+ // Helper to convert to simple material (stub)
1739
+ function toSimpleMaterial(mat) {
1740
+ // Basic implementation, preserve color/map
1741
+ const m = new THREE.MeshBasicMaterial();
1742
+ if (mat.color)
1743
+ m.color.copy(mat.color);
1744
+ if (mat.map)
1745
+ m.map = mat.map;
1746
+ return m;
1747
+ }
1651
1748
 
1652
- /** 默认值 */
1749
+ /**
1750
+ * @file skyboxLoader.ts
1751
+ * @description
1752
+ * Utility for loading skyboxes (CubeTexture or Equirectangular/HDR).
1753
+ *
1754
+ * @best-practice
1755
+ * - Use `loadSkybox` for a unified interface.
1756
+ * - Supports internal caching to avoid reloading the same skybox.
1757
+ * - Can set background and environment map independently.
1758
+ */
1759
+ /** Default Values */
1653
1760
  const DEFAULT_OPTIONS = {
1654
1761
  setAsBackground: true,
1655
1762
  setAsEnvironment: true,
1656
1763
  useSRGBEncoding: true,
1657
1764
  cache: true
1658
1765
  };
1659
- /** 内部缓存:key -> { handle, refCount } */
1766
+ /** Internal Cache: key -> { handle, refCount } */
1660
1767
  const cubeCache = new Map();
1661
1768
  const equirectCache = new Map();
1662
1769
  /* -------------------------------------------
1663
- 公共函数:加载 skybox(自动选 cube equirect
1770
+ Public Function: Load skybox (Automatically choose cube or equirect)
1664
1771
  ------------------------------------------- */
1665
1772
  /**
1666
- * 加载立方体贴图(6张)
1667
- * @param renderer THREE.WebGLRenderer - 用于 PMREM 生成环境贴图
1773
+ * Load Cube Texture (6 images)
1774
+ * @param renderer THREE.WebGLRenderer - Used for PMREM generating environment map
1668
1775
  * @param scene THREE.Scene
1669
- * @param paths string[] 6 张图片地址,顺序:[px, nx, py, ny, pz, nz]
1776
+ * @param paths string[] 6 image paths, order: [px, nx, py, ny, pz, nz]
1670
1777
  * @param opts SkyboxOptions
1671
1778
  */
1672
1779
  function loadCubeSkybox(renderer_1, scene_1, paths_1) {
@@ -1676,7 +1783,7 @@ function loadCubeSkybox(renderer_1, scene_1, paths_1) {
1676
1783
  if (!Array.isArray(paths) || paths.length !== 6)
1677
1784
  throw new Error('cube skybox requires 6 image paths');
1678
1785
  const key = paths.join('|');
1679
- // 缓存处理
1786
+ // Cache handling
1680
1787
  if (options.cache && cubeCache.has(key)) {
1681
1788
  const rec = cubeCache.get(key);
1682
1789
  rec.refCount += 1;
@@ -1687,12 +1794,12 @@ function loadCubeSkybox(renderer_1, scene_1, paths_1) {
1687
1794
  scene.environment = rec.handle.envRenderTarget.texture;
1688
1795
  return rec.handle;
1689
1796
  }
1690
- // 加载立方体贴图
1797
+ // Load cube texture
1691
1798
  const loader = new THREE.CubeTextureLoader();
1692
1799
  const texture = yield new Promise((resolve, reject) => {
1693
1800
  loader.load(paths, (tex) => resolve(tex), undefined, (err) => reject(err));
1694
1801
  });
1695
- // 设置编码与映射
1802
+ // Set encoding and mapping
1696
1803
  if (options.useSRGBEncoding)
1697
1804
  texture.encoding = THREE.sRGBEncoding;
1698
1805
  texture.mapping = THREE.CubeReflectionMapping;
@@ -1765,7 +1872,7 @@ function loadCubeSkybox(renderer_1, scene_1, paths_1) {
1765
1872
  });
1766
1873
  }
1767
1874
  /**
1768
- * 加载等距/单图(支持 HDR via RGBELoader
1875
+ * Load Equirectangular/Single Image (Supports HDR via RGBELoader)
1769
1876
  * @param renderer THREE.WebGLRenderer
1770
1877
  * @param scene THREE.Scene
1771
1878
  * @param url string - *.hdr, *.exr, *.jpg, *.png
@@ -1785,7 +1892,7 @@ function loadEquirectSkybox(renderer_1, scene_1, url_1) {
1785
1892
  scene.environment = rec.handle.envRenderTarget.texture;
1786
1893
  return rec.handle;
1787
1894
  }
1788
- // 动态导入 RGBELoader(用于 .hdr/.exr),如果加载的是普通 jpg/png 可直接用 TextureLoader
1895
+ // Dynamically import RGBELoader (for .hdr/.exr), if loading normal jpg/png directly use TextureLoader
1789
1896
  const isHDR = /\.hdr$|\.exr$/i.test(url);
1790
1897
  let hdrTexture;
1791
1898
  if (isHDR) {
@@ -1862,9 +1969,9 @@ function loadSkybox(renderer_1, scene_1, params_1) {
1862
1969
  });
1863
1970
  }
1864
1971
  /* -------------------------
1865
- 缓存/引用计数 辅助方法
1972
+ Cache / Reference Counting Helper Methods
1866
1973
  ------------------------- */
1867
- /** 释放一个缓存的 skybox(会减少 refCountrefCount=0 时才真正 dispose) */
1974
+ /** Release a cached skybox (decrements refCount, only truly disposes when refCount=0) */
1868
1975
  function releaseSkybox(handle) {
1869
1976
  // check cube cache
1870
1977
  if (cubeCache.has(handle.key)) {
@@ -1889,85 +1996,94 @@ function releaseSkybox(handle) {
1889
1996
  // handle.dispose()
1890
1997
  }
1891
1998
 
1892
- // utils/BlueSkyManager.ts - 优化版
1893
1999
  /**
1894
- * BlueSkyManager - 优化版
2000
+ * @file blueSkyManager.ts
2001
+ * @description
2002
+ * Global singleton manager for loading and managing HDR/EXR blue sky environment maps.
2003
+ *
2004
+ * @best-practice
2005
+ * - Call `init` once before use.
2006
+ * - Use `loadAsync` to load skyboxes with progress tracking.
2007
+ * - Automatically handles PMREM generation for realistic lighting.
2008
+ */
2009
+ /**
2010
+ * BlueSkyManager - Optimized
1895
2011
  * ---------------------------------------------------------
1896
- * 一个全局单例管理器,用于加载和管理基于 HDR/EXR 的蓝天白云环境贴图。
2012
+ * A global singleton manager for loading and managing HDR/EXR based blue sky environment maps.
1897
2013
  *
1898
- * ✨ 优化内容:
1899
- * - 添加加载进度回调
1900
- * - 支持加载取消
1901
- * - 完善错误处理
1902
- * - 返回 Promise 支持异步
1903
- * - 添加加载状态管理
2014
+ * Features:
2015
+ * - Adds load progress callback
2016
+ * - Supports load cancellation
2017
+ * - Improved error handling
2018
+ * - Returns Promise for async operation
2019
+ * - Adds loading state management
1904
2020
  */
1905
2021
  class BlueSkyManager {
1906
2022
  constructor() {
1907
- /** 当前环境贴图的 RenderTarget,用于后续释放 */
2023
+ /** RenderTarget for current environment map, used for subsequent disposal */
1908
2024
  this.skyRT = null;
1909
- /** 是否已经初始化 */
2025
+ /** Whether already initialized */
1910
2026
  this.isInitialized = false;
1911
- /** 当前加载器,用于取消加载 */
2027
+ /** Current loader, used for cancelling load */
1912
2028
  this.currentLoader = null;
1913
- /** 加载状态 */
2029
+ /** Loading state */
1914
2030
  this.loadingState = 'idle';
1915
2031
  }
1916
2032
  /**
1917
- * 初始化
2033
+ * Initialize
1918
2034
  * ---------------------------------------------------------
1919
- * 必须在使用 BlueSkyManager 之前调用一次。
1920
- * @param renderer WebGLRenderer 实例
1921
- * @param scene Three.js 场景
1922
- * @param exposure 曝光度 (默认 1.0)
2035
+ * Must be called once before using BlueSkyManager.
2036
+ * @param renderer WebGLRenderer instance
2037
+ * @param scene Three.js Scene
2038
+ * @param exposure Exposure (default 1.0)
1923
2039
  */
1924
2040
  init(renderer, scene, exposure = 1.0) {
1925
2041
  if (this.isInitialized) {
1926
- console.warn('BlueSkyManager: 已经初始化,跳过重复初始化');
2042
+ console.warn('BlueSkyManager: Already initialized, skipping duplicate initialization');
1927
2043
  return;
1928
2044
  }
1929
2045
  this.renderer = renderer;
1930
2046
  this.scene = scene;
1931
- // 使用 ACESFilmicToneMapping,效果更接近真实
2047
+ // Use ACESFilmicToneMapping, effect is closer to reality
1932
2048
  this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
1933
2049
  this.renderer.toneMappingExposure = exposure;
1934
- // 初始化 PMREM 生成器(全局只需一个)
2050
+ // Initialize PMREM generator (only one needed globally)
1935
2051
  this.pmremGen = new THREE.PMREMGenerator(renderer);
1936
2052
  this.pmremGen.compileEquirectangularShader();
1937
2053
  this.isInitialized = true;
1938
2054
  }
1939
2055
  /**
1940
- * 加载蓝天 HDR/EXR 贴图并应用到场景(Promise 版本)
2056
+ * Load blue sky HDR/EXR map and apply to scene (Promise version)
1941
2057
  * ---------------------------------------------------------
1942
- * @param exrPath HDR/EXR 文件路径
1943
- * @param options 加载选项
2058
+ * @param exrPath HDR/EXR file path
2059
+ * @param options Load options
1944
2060
  * @returns Promise<void>
1945
2061
  */
1946
2062
  loadAsync(exrPath, options = {}) {
1947
2063
  if (!this.isInitialized) {
1948
2064
  return Promise.reject(new Error('BlueSkyManager not initialized!'));
1949
2065
  }
1950
- // 取消之前的加载
2066
+ // Cancel previous load
1951
2067
  this.cancelLoad();
1952
2068
  const { background = true, onProgress, onComplete, onError } = options;
1953
2069
  this.loadingState = 'loading';
1954
2070
  this.currentLoader = new EXRLoader();
1955
2071
  return new Promise((resolve, reject) => {
1956
2072
  this.currentLoader.load(exrPath,
1957
- // 成功回调
2073
+ // Success callback
1958
2074
  (texture) => {
1959
2075
  try {
1960
- // 设置贴图为球面反射映射
2076
+ // Set texture mapping to EquirectangularReflectionMapping
1961
2077
  texture.mapping = THREE.EquirectangularReflectionMapping;
1962
- // 清理旧的环境贴图
2078
+ // Clear old environment map
1963
2079
  this.dispose();
1964
- // PMREM 生成高效的环境贴图
2080
+ // Generate efficient environment map using PMREM
1965
2081
  this.skyRT = this.pmremGen.fromEquirectangular(texture);
1966
- // 应用到场景:环境光照 & 背景
2082
+ // Apply to scene: Environment Lighting & Background
1967
2083
  this.scene.environment = this.skyRT.texture;
1968
2084
  if (background)
1969
2085
  this.scene.background = this.skyRT.texture;
1970
- // 原始 HDR/EXR 贴图用完即销毁,节省内存
2086
+ // Dispose original HDR/EXR texture immediately to save memory
1971
2087
  texture.dispose();
1972
2088
  this.loadingState = 'loaded';
1973
2089
  this.currentLoader = null;
@@ -1985,14 +2101,14 @@ class BlueSkyManager {
1985
2101
  reject(error);
1986
2102
  }
1987
2103
  },
1988
- // 进度回调
2104
+ // Progress callback
1989
2105
  (xhr) => {
1990
2106
  if (onProgress && xhr.lengthComputable) {
1991
2107
  const progress = xhr.loaded / xhr.total;
1992
2108
  onProgress(progress);
1993
2109
  }
1994
2110
  },
1995
- // 错误回调
2111
+ // Error callback
1996
2112
  (err) => {
1997
2113
  this.loadingState = 'error';
1998
2114
  this.currentLoader = null;
@@ -2004,10 +2120,10 @@ class BlueSkyManager {
2004
2120
  });
2005
2121
  }
2006
2122
  /**
2007
- * 加载蓝天 HDR/EXR 贴图并应用到场景(同步 API,保持向后兼容)
2123
+ * Load blue sky HDR/EXR map and apply to scene (Sync API, for backward compatibility)
2008
2124
  * ---------------------------------------------------------
2009
- * @param exrPath HDR/EXR 文件路径
2010
- * @param background 是否应用为场景背景 (默认 true)
2125
+ * @param exrPath HDR/EXR file path
2126
+ * @param background Whether to apply as scene background (default true)
2011
2127
  */
2012
2128
  load(exrPath, background = true) {
2013
2129
  this.loadAsync(exrPath, { background }).catch((error) => {
@@ -2015,32 +2131,32 @@ class BlueSkyManager {
2015
2131
  });
2016
2132
  }
2017
2133
  /**
2018
- * 取消当前加载
2134
+ * Cancel current load
2019
2135
  */
2020
2136
  cancelLoad() {
2021
2137
  if (this.currentLoader) {
2022
- // EXRLoader 本身没有 abort 方法,但我们可以清空引用
2138
+ // EXRLoader itself does not have abort method, but we can clear the reference
2023
2139
  this.currentLoader = null;
2024
2140
  this.loadingState = 'idle';
2025
2141
  }
2026
2142
  }
2027
2143
  /**
2028
- * 获取加载状态
2144
+ * Get loading state
2029
2145
  */
2030
2146
  getLoadingState() {
2031
2147
  return this.loadingState;
2032
2148
  }
2033
2149
  /**
2034
- * 是否正在加载
2150
+ * Is loading
2035
2151
  */
2036
2152
  isLoading() {
2037
2153
  return this.loadingState === 'loading';
2038
2154
  }
2039
2155
  /**
2040
- * 释放当前的天空贴图资源
2156
+ * Release current sky texture resources
2041
2157
  * ---------------------------------------------------------
2042
- * 仅清理 skyRT,不销毁 PMREM
2043
- * 适用于切换 HDR/EXR 文件时调用
2158
+ * Only cleans up skyRT, does not destroy PMREM
2159
+ * Suitable for calling when switching HDR/EXR files
2044
2160
  */
2045
2161
  dispose() {
2046
2162
  if (this.skyRT) {
@@ -2054,10 +2170,10 @@ class BlueSkyManager {
2054
2170
  this.scene.environment = null;
2055
2171
  }
2056
2172
  /**
2057
- * 完全销毁 BlueSkyManager
2173
+ * Completely destroy BlueSkyManager
2058
2174
  * ---------------------------------------------------------
2059
- * 包括 PMREMGenerator 的销毁
2060
- * 通常在场景彻底销毁或应用退出时调用
2175
+ * Includes destruction of PMREMGenerator
2176
+ * Usually called when the scene is completely destroyed or the application exits
2061
2177
  */
2062
2178
  destroy() {
2063
2179
  var _a;
@@ -2069,22 +2185,32 @@ class BlueSkyManager {
2069
2185
  }
2070
2186
  }
2071
2187
  /**
2072
- * 🌐 全局单例
2188
+ * Global Singleton
2073
2189
  * ---------------------------------------------------------
2074
- * 直接导出一个全局唯一的 BlueSkyManager 实例,
2075
- * 保证整个应用中只用一个 PMREMGenerator,性能最佳。
2190
+ * Directly export a globally unique BlueSkyManager instance,
2191
+ * Ensuring only one PMREMGenerator is used throughout the application for best performance.
2076
2192
  */
2077
2193
  const BlueSky = new BlueSkyManager();
2078
2194
 
2079
2195
  /**
2080
- * 创建模型标签(带连线和脉冲圆点)- 优化版
2196
+ * @file modelsLabel.ts
2197
+ * @description
2198
+ * Creates interactive 2D labels (DOM elements) attached to 3D objects with connecting lines.
2199
+ *
2200
+ * @best-practice
2201
+ * - Use `createModelsLabel` to annotate parts of a model.
2202
+ * - Supports fading endpoints, pulsing dots, and custom styling.
2203
+ * - Performance optimized with caching and RAF throttling.
2204
+ */
2205
+ /**
2206
+ * Create Model Labels (with connecting lines and pulsing dots) - Optimized
2081
2207
  *
2082
- * ✨ 优化内容:
2083
- * - 支持暂停/恢复更新
2084
- * - 可配置更新间隔
2085
- * - 淡入淡出效果
2086
- * - 缓存包围盒计算
2087
- * - RAF 管理优化
2208
+ * Features:
2209
+ * - Supports pause/resume
2210
+ * - Configurable update interval
2211
+ * - Fade in/out effects
2212
+ * - Cached bounding box calculation
2213
+ * - RAF management optimization
2088
2214
  */
2089
2215
  function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, options) {
2090
2216
  var _a, _b, _c, _d, _e, _f;
@@ -2099,8 +2225,8 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2099
2225
  dotSpacing: (_c = options === null || options === void 0 ? void 0 : options.dotSpacing) !== null && _c !== void 0 ? _c : 2,
2100
2226
  lineColor: (options === null || options === void 0 ? void 0 : options.lineColor) || 'rgba(200,200,200,0.7)',
2101
2227
  lineWidth: (_d = options === null || options === void 0 ? void 0 : options.lineWidth) !== null && _d !== void 0 ? _d : 1,
2102
- updateInterval: (_e = options === null || options === void 0 ? void 0 : options.updateInterval) !== null && _e !== void 0 ? _e : 0, // 默认每帧更新
2103
- fadeInDuration: (_f = options === null || options === void 0 ? void 0 : options.fadeInDuration) !== null && _f !== void 0 ? _f : 300, // 淡入时长
2228
+ updateInterval: (_e = options === null || options === void 0 ? void 0 : options.updateInterval) !== null && _e !== void 0 ? _e : 0, // Default update every frame
2229
+ fadeInDuration: (_f = options === null || options === void 0 ? void 0 : options.fadeInDuration) !== null && _f !== void 0 ? _f : 300, // Fade-in duration
2104
2230
  };
2105
2231
  const container = document.createElement('div');
2106
2232
  container.style.position = 'absolute';
@@ -2126,10 +2252,10 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2126
2252
  let currentLabelsMap = Object.assign({}, modelLabelsMap);
2127
2253
  let labels = [];
2128
2254
  let isActive = true;
2129
- let isPaused = false; // ✨ 暂停状态
2130
- let rafId = null; // ✨ RAF ID
2131
- let lastUpdateTime = 0; // ✨ 上次更新时间
2132
- // 注入样式(带淡入动画)
2255
+ let isPaused = false;
2256
+ let rafId = null;
2257
+ let lastUpdateTime = 0;
2258
+ // Inject styles (with fade-in animation)
2133
2259
  const styleId = 'three-model-label-styles';
2134
2260
  if (!document.getElementById(styleId)) {
2135
2261
  const style = document.createElement('style');
@@ -2168,14 +2294,14 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2168
2294
  `;
2169
2295
  document.head.appendChild(style);
2170
2296
  }
2171
- // 获取或更新缓存的顶部位置
2297
+ // Get or update cached top position
2172
2298
  const getObjectTopPosition = (labelData) => {
2173
2299
  const obj = labelData.object;
2174
- // 如果有缓存且对象没有变换,直接返回
2300
+ // If cached and object hasn't transformed, return cached
2175
2301
  if (labelData.cachedTopPos && !obj.matrixWorldNeedsUpdate) {
2176
2302
  return labelData.cachedTopPos.clone();
2177
2303
  }
2178
- // 重新计算
2304
+ // Recalculate
2179
2305
  const box = new THREE.Box3().setFromObject(obj);
2180
2306
  labelData.cachedBox = box;
2181
2307
  if (!box.isEmpty()) {
@@ -2253,20 +2379,20 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2253
2379
  wrapper,
2254
2380
  dot,
2255
2381
  line,
2256
- cachedBox: null, // 初始化缓存
2382
+ cachedBox: null, // Initialize cache
2257
2383
  cachedTopPos: null
2258
2384
  });
2259
2385
  }
2260
2386
  });
2261
2387
  };
2262
2388
  rebuildLabels();
2263
- // 优化的更新函数
2389
+ // Optimized update function
2264
2390
  const updateLabels = (timestamp) => {
2265
2391
  if (!isActive || isPaused) {
2266
2392
  rafId = null;
2267
2393
  return;
2268
2394
  }
2269
- // ✨ 节流处理
2395
+ // Throttle
2270
2396
  if (cfg.updateInterval > 0 && timestamp - lastUpdateTime < cfg.updateInterval) {
2271
2397
  rafId = requestAnimationFrame(updateLabels);
2272
2398
  return;
@@ -2279,7 +2405,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2279
2405
  svg.setAttribute('height', `${height}`);
2280
2406
  labels.forEach((labelData) => {
2281
2407
  const { el, wrapper, dot, line } = labelData;
2282
- const topWorld = getObjectTopPosition(labelData); // 使用缓存
2408
+ const topWorld = getObjectTopPosition(labelData); // Use cache
2283
2409
  const topNDC = topWorld.clone().project(camera);
2284
2410
  const modelX = (topNDC.x * 0.5 + 0.5) * width + rect.left;
2285
2411
  const modelY = (-(topNDC.y * 0.5) + 0.5) * height + rect.top;
@@ -2314,7 +2440,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2314
2440
  currentLabelsMap = Object.assign({}, newMap);
2315
2441
  rebuildLabels();
2316
2442
  },
2317
- // 暂停更新
2443
+ // Pause update
2318
2444
  pause() {
2319
2445
  isPaused = true;
2320
2446
  if (rafId !== null) {
@@ -2322,7 +2448,7 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2322
2448
  rafId = null;
2323
2449
  }
2324
2450
  },
2325
- // 恢复更新
2451
+ // Resume update
2326
2452
  resume() {
2327
2453
  if (!isPaused)
2328
2454
  return;
@@ -2343,10 +2469,34 @@ function createModelsLabel(camera, renderer, parentModel, modelLabelsMap, option
2343
2469
  };
2344
2470
  }
2345
2471
 
2472
+ /**
2473
+ * @file exploder.ts
2474
+ * @description
2475
+ * GroupExploder - Three.js based model explosion effect tool (Vue3 + TS Support)
2476
+ * ----------------------------------------------------------------------
2477
+ * This tool is used to perform "explode / restore" animations on a set of specified Meshes:
2478
+ * - Initialize only once (onMounted)
2479
+ * - Supports dynamic switching of models and automatically restores the explosion state of the previous model
2480
+ * - Supports multiple arrangement modes (ring / spiral / grid / radial)
2481
+ * - Supports automatic transparency for non-exploded objects (dimOthers)
2482
+ * - Supports automatic camera positioning to the best observation point
2483
+ * - All animations use native requestAnimationFrame
2484
+ *
2485
+ * @best-practice
2486
+ * - Initialize in `onMounted`.
2487
+ * - Use `setMeshes` to update the active set of meshes to explode.
2488
+ * - Call `explode()` to trigger the effect and `restore()` to reset.
2489
+ */
2346
2490
  function easeInOutQuad(t) {
2347
2491
  return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
2348
2492
  }
2349
2493
  class GroupExploder {
2494
+ /**
2495
+ * Constructor
2496
+ * @param scene Three.js Scene instance
2497
+ * @param camera Three.js Camera (usually PerspectiveCamera)
2498
+ * @param controls OrbitControls instance (must be bound to camera)
2499
+ */
2350
2500
  constructor(scene, camera, controls) {
2351
2501
  // sets and snapshots
2352
2502
  this.currentSet = null;
@@ -2378,10 +2528,12 @@ class GroupExploder {
2378
2528
  this.log('init() called');
2379
2529
  }
2380
2530
  /**
2381
- * setMeshes(newSet):
2531
+ * Set the current set of meshes for explosion.
2382
2532
  * - Detects content-level changes even if same Set reference is used.
2383
2533
  * - Preserves prevSet/stateMap to allow async restore when needed.
2384
2534
  * - Ensures stateMap contains snapshots for *all meshes in the new set*.
2535
+ * @param newSet The new set of meshes
2536
+ * @param contextId Optional context ID to distinguish business scenarios
2385
2537
  */
2386
2538
  setMeshes(newSet, options) {
2387
2539
  return __awaiter(this, void 0, void 0, function* () {
@@ -2583,6 +2735,11 @@ class GroupExploder {
2583
2735
  return;
2584
2736
  });
2585
2737
  }
2738
+ /**
2739
+ * Restore all exploded meshes to their original transform:
2740
+ * - Supports smooth animation
2741
+ * - Automatically cancels transparency
2742
+ */
2586
2743
  restore(duration = 400) {
2587
2744
  if (!this.currentSet || this.currentSet.size === 0) {
2588
2745
  this.log('restore: no currentSet to restore');
@@ -2939,120 +3096,142 @@ class GroupExploder {
2939
3096
  return targets;
2940
3097
  }
2941
3098
  animateCameraToFit(targetCenter, targetRadius, opts) {
2942
- var _a, _b, _c;
3099
+ var _a, _b, _c, _d;
2943
3100
  const duration = (_a = opts === null || opts === void 0 ? void 0 : opts.duration) !== null && _a !== void 0 ? _a : 600;
2944
3101
  const padding = (_b = opts === null || opts === void 0 ? void 0 : opts.padding) !== null && _b !== void 0 ? _b : 1.5;
2945
3102
  if (!(this.camera instanceof THREE.PerspectiveCamera)) {
2946
3103
  if (this.controls && this.controls.target) {
2947
- this.controls.target.copy(targetCenter);
2948
- if (typeof this.controls.update === 'function')
2949
- this.controls.update();
3104
+ // Fallback for non-PerspectiveCamera
3105
+ const startTarget = this.controls.target.clone();
3106
+ const startPos = this.camera.position.clone();
3107
+ const endTarget = targetCenter.clone();
3108
+ const dir = startPos.clone().sub(startTarget).normalize();
3109
+ const dist = startPos.distanceTo(startTarget);
3110
+ const endPos = endTarget.clone().add(dir.multiplyScalar(dist));
3111
+ const startTime = performance.now();
3112
+ const tick = (now) => {
3113
+ var _a;
3114
+ const t = Math.min(1, (now - startTime) / duration);
3115
+ const k = easeInOutQuad(t);
3116
+ if (this.controls && this.controls.target) {
3117
+ this.controls.target.lerpVectors(startTarget, endTarget, k);
3118
+ }
3119
+ this.camera.position.lerpVectors(startPos, endPos, k);
3120
+ if ((_a = this.controls) === null || _a === void 0 ? void 0 : _a.update)
3121
+ this.controls.update();
3122
+ if (t < 1) {
3123
+ this.cameraAnimId = requestAnimationFrame(tick);
3124
+ }
3125
+ else {
3126
+ this.cameraAnimId = null;
3127
+ }
3128
+ };
3129
+ this.cameraAnimId = requestAnimationFrame(tick);
2950
3130
  }
2951
3131
  return Promise.resolve();
2952
3132
  }
2953
- const cam = this.camera;
2954
- const fov = (cam.fov * Math.PI) / 180;
2955
- const safeRadius = isFinite(targetRadius) && targetRadius > 0 ? targetRadius : 1;
2956
- const desiredDistance = Math.min(1e6, (safeRadius * ((_c = opts === null || opts === void 0 ? void 0 : opts.padding) !== null && _c !== void 0 ? _c : padding)) / Math.sin(fov / 2));
2957
- const camPos = cam.position.clone();
2958
- const dir = camPos.clone().sub(targetCenter);
2959
- if (dir.length() === 0)
3133
+ // PerspectiveCamera logic
3134
+ const fov = THREE.MathUtils.degToRad(this.camera.fov);
3135
+ const aspect = this.camera.aspect;
3136
+ // Calculate distance needed to fit the sphere
3137
+ // tan(fov/2) = radius / distance => distance = radius / tan(fov/2)
3138
+ // We also consider aspect ratio for horizontal fit
3139
+ const distV = targetRadius / Math.sin(fov / 2);
3140
+ const distH = targetRadius / Math.sin(Math.min(fov, fov * aspect) / 2); // approximate
3141
+ const dist = Math.max(distV, distH) * padding;
3142
+ const startPos = this.camera.position.clone();
3143
+ const startTarget = ((_c = this.controls) === null || _c === void 0 ? void 0 : _c.target) ? this.controls.target.clone() : new THREE.Vector3(); // assumption
3144
+ if (!((_d = this.controls) === null || _d === void 0 ? void 0 : _d.target)) {
3145
+ this.camera.getWorldDirection(startTarget);
3146
+ startTarget.add(startPos);
3147
+ }
3148
+ // Determine end position: keep current viewing direction relative to center
3149
+ const dir = startPos.clone().sub(startTarget).normalize();
3150
+ if (dir.lengthSq() < 0.001)
2960
3151
  dir.set(0, 0, 1);
2961
- else
2962
- dir.normalize();
2963
- const newCamPos = targetCenter.clone().add(dir.multiplyScalar(desiredDistance));
2964
- const startPos = cam.position.clone();
2965
- const startTarget = (this.controls && this.controls.target) ? (this.controls.target.clone()) : this.getCameraLookAtPoint();
2966
3152
  const endTarget = targetCenter.clone();
2967
- const startTime = performance.now();
3153
+ const endPos = endTarget.clone().add(dir.multiplyScalar(dist));
2968
3154
  return new Promise((resolve) => {
3155
+ const startTime = performance.now();
2969
3156
  const tick = (now) => {
2970
- const t = Math.min(1, (now - startTime) / Math.max(1, duration));
2971
- const eased = easeInOutQuad(t);
2972
- cam.position.lerpVectors(startPos, newCamPos, eased);
2973
- if (this.controls && this.controls.target)
2974
- this.controls.target.lerpVectors(startTarget, endTarget, eased);
2975
- cam.updateProjectionMatrix();
2976
- if (this.controls && typeof this.controls.update === 'function')
2977
- this.controls.update();
2978
- if (t < 1)
3157
+ var _a, _b;
3158
+ const t = Math.min(1, (now - startTime) / duration);
3159
+ const k = easeInOutQuad(t);
3160
+ this.camera.position.lerpVectors(startPos, endPos, k);
3161
+ if (this.controls && this.controls.target) {
3162
+ this.controls.target.lerpVectors(startTarget, endTarget, k);
3163
+ (_b = (_a = this.controls).update) === null || _b === void 0 ? void 0 : _b.call(_a);
3164
+ }
3165
+ else {
3166
+ this.camera.lookAt(endTarget); // simple lookAt if no controls
3167
+ }
3168
+ if (t < 1) {
2979
3169
  this.cameraAnimId = requestAnimationFrame(tick);
3170
+ }
2980
3171
  else {
2981
3172
  this.cameraAnimId = null;
2982
- this.log(`animateCameraToFit: done. center=${targetCenter.toArray().map((n) => n.toFixed(2))}, radius=${targetRadius.toFixed(2)}`);
2983
3173
  resolve();
2984
3174
  }
2985
3175
  };
2986
3176
  this.cameraAnimId = requestAnimationFrame(tick);
2987
3177
  });
2988
3178
  }
2989
- getCameraLookAtPoint() {
2990
- const dir = new THREE.Vector3();
2991
- this.camera.getWorldDirection(dir);
2992
- return this.camera.position.clone().add(dir.multiplyScalar(10));
2993
- }
3179
+ /**
3180
+ * Cancel all running animations
3181
+ */
2994
3182
  cancelAnimations() {
2995
- if (this.animId) {
3183
+ if (this.animId !== null) {
2996
3184
  cancelAnimationFrame(this.animId);
2997
3185
  this.animId = null;
2998
3186
  }
2999
- if (this.cameraAnimId) {
3187
+ if (this.cameraAnimId !== null) {
3000
3188
  cancelAnimationFrame(this.cameraAnimId);
3001
3189
  this.cameraAnimId = null;
3002
3190
  }
3003
3191
  }
3192
+ /**
3193
+ * Dispose: remove listener, cancel animation, clear references
3194
+ */
3004
3195
  dispose() {
3005
- return __awaiter(this, arguments, void 0, function* (restoreBefore = true) {
3006
- this.cancelAnimations();
3007
- if (restoreBefore && this.isExploded) {
3008
- try {
3009
- yield this.restore(200);
3010
- }
3011
- catch (_a) { }
3012
- }
3013
- // force restore of materials
3014
- for (const [mat, ctxs] of Array.from(this.materialContexts.entries())) {
3015
- const snap = this.materialSnaps.get(mat);
3016
- if (snap) {
3017
- mat.transparent = snap.transparent;
3018
- mat.opacity = snap.opacity;
3019
- if (typeof snap.depthWrite !== 'undefined')
3020
- mat.depthWrite = snap.depthWrite;
3021
- mat.needsUpdate = true;
3022
- }
3023
- this.materialContexts.delete(mat);
3024
- this.materialSnaps.delete(mat);
3025
- }
3026
- this.contextMaterials.clear();
3027
- this.stateMap.clear();
3028
- this.prevStateMap.clear();
3029
- this.currentSet = null;
3030
- this.prevSet = null;
3031
- this.isInitialized = false;
3032
- this.isExploded = false;
3033
- this.log('dispose: cleaned up');
3034
- });
3196
+ this.cancelAnimations();
3197
+ this.currentSet = null;
3198
+ this.prevSet = null;
3199
+ this.stateMap.clear();
3200
+ this.prevStateMap.clear();
3201
+ this.materialContexts.clear();
3202
+ this.materialSnaps.clear();
3203
+ this.contextMaterials.clear();
3204
+ this.log('dispose() called, resources cleaned up');
3035
3205
  }
3036
3206
  }
3037
3207
 
3038
3208
  /**
3039
- * 自动设置相机与基础灯光 - 优化版
3209
+ * @file autoSetup.ts
3210
+ * @description
3211
+ * Automatically sets up the camera and basic lighting scene based on the model's bounding box.
3212
+ *
3213
+ * @best-practice
3214
+ * - Call `autoSetupCameraAndLight` after loading a model to get a quick "good looking" scene.
3215
+ * - Returns a handle to dispose lights or update intensity later.
3216
+ */
3217
+ /**
3218
+ * Automatically setup camera and basic lighting - Optimized
3040
3219
  *
3041
- * ✨ 优化内容:
3042
- * - 添加灯光强度调整方法
3043
- * - 完善错误处理
3044
- * - 优化dispose逻辑
3220
+ * Features:
3221
+ * - Adds light intensity adjustment method
3222
+ * - Improved error handling
3223
+ * - Optimized dispose logic
3045
3224
  *
3046
- * - camera: THREE.PerspectiveCamera(会被移动并指向模型中心)
3047
- * - scene: THREE.Scene(会把新创建的 light group 加入 scene
3048
- * - model: THREE.Object3D 已加载的模型(任意 transform/坐标)
3049
- * - options: 可选配置(见 AutoSetupOptions
3225
+ * - camera: THREE.PerspectiveCamera (will be moved and pointed at model center)
3226
+ * - scene: THREE.Scene (newly created light group will be added to the scene)
3227
+ * - model: THREE.Object3D loaded model (arbitrary transform/coordinates)
3228
+ * - options: Optional configuration (see AutoSetupOptions)
3050
3229
  *
3051
- * 返回 AutoSetupHandle,调用方在组件卸载/切换时请调用 handle.dispose()
3230
+ * Returns AutoSetupHandle, caller should call handle.dispose() when component unmounts/switches
3052
3231
  */
3053
3232
  function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3054
3233
  var _a, _b, _c, _d, _e, _f, _g;
3055
- // 边界检查
3234
+ // Boundary check
3056
3235
  if (!camera || !scene || !model) {
3057
3236
  throw new Error('autoSetupCameraAndLight: camera, scene, model are required');
3058
3237
  }
@@ -3066,9 +3245,9 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3066
3245
  renderer: (_g = options.renderer) !== null && _g !== void 0 ? _g : null,
3067
3246
  };
3068
3247
  try {
3069
- // --- 1) 计算包围数据
3248
+ // --- 1) Calculate bounding data
3070
3249
  const box = new THREE.Box3().setFromObject(model);
3071
- // 检查包围盒有效性
3250
+ // Check bounding box validity
3072
3251
  if (!isFinite(box.min.x)) {
3073
3252
  throw new Error('autoSetupCameraAndLight: Invalid bounding box');
3074
3253
  }
@@ -3076,7 +3255,7 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3076
3255
  box.getBoundingSphere(sphere);
3077
3256
  const center = sphere.center.clone();
3078
3257
  const radius = Math.max(0.001, sphere.radius);
3079
- // --- 2) 计算相机位置
3258
+ // --- 2) Calculate camera position
3080
3259
  const fov = (camera.fov * Math.PI) / 180;
3081
3260
  const halfFov = fov / 2;
3082
3261
  const sinHalfFov = Math.max(Math.sin(halfFov), 0.001);
@@ -3088,17 +3267,17 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3088
3267
  camera.near = Math.max(0.001, radius / 1000);
3089
3268
  camera.far = Math.max(1000, radius * 50);
3090
3269
  camera.updateProjectionMatrix();
3091
- // --- 3) 启用阴影
3270
+ // --- 3) Enable Shadows
3092
3271
  if (opts.renderer && opts.enableShadows) {
3093
3272
  opts.renderer.shadowMap.enabled = true;
3094
3273
  opts.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
3095
3274
  }
3096
- // --- 4) 创建灯光组
3275
+ // --- 4) Create Lights Group
3097
3276
  const lightsGroup = new THREE.Group();
3098
3277
  lightsGroup.name = 'autoSetupLightsGroup';
3099
3278
  lightsGroup.position.copy(center);
3100
3279
  scene.add(lightsGroup);
3101
- // 4.1 基础光
3280
+ // 4.1 Basic Light
3102
3281
  const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 0.6);
3103
3282
  hemi.name = 'auto_hemi';
3104
3283
  hemi.position.set(0, radius * 2.0, 0);
@@ -3106,7 +3285,7 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3106
3285
  const ambient = new THREE.AmbientLight(0xffffff, 0.25);
3107
3286
  ambient.name = 'auto_ambient';
3108
3287
  lightsGroup.add(ambient);
3109
- // 4.2 方向光
3288
+ // 4.2 Directional Lights
3110
3289
  const dirCount = Math.max(1, Math.floor(opts.directionalCount));
3111
3290
  const directionalLights = [];
3112
3291
  const dirs = [];
@@ -3141,7 +3320,7 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3141
3320
  }
3142
3321
  directionalLights.push(light);
3143
3322
  }
3144
- // 4.3 点光补光
3323
+ // 4.3 Point Light Fill
3145
3324
  const fill1 = new THREE.PointLight(0xffffff, 0.5, radius * 4);
3146
3325
  fill1.position.copy(center).add(new THREE.Vector3(radius * 0.5, 0.2 * radius, 0));
3147
3326
  fill1.name = 'auto_fill1';
@@ -3150,7 +3329,7 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3150
3329
  fill2.position.copy(center).add(new THREE.Vector3(-radius * 0.5, -0.2 * radius, 0));
3151
3330
  fill2.name = 'auto_fill2';
3152
3331
  lightsGroup.add(fill2);
3153
- // --- 5) 设置 Mesh 阴影属性
3332
+ // --- 5) Set Mesh Shadow Props
3154
3333
  if (opts.setMeshShadowProps) {
3155
3334
  model.traverse((ch) => {
3156
3335
  if (ch.isMesh) {
@@ -3161,12 +3340,12 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3161
3340
  }
3162
3341
  });
3163
3342
  }
3164
- // --- 6) 返回 handle ---
3343
+ // --- 6) Return handle ---
3165
3344
  const handle = {
3166
3345
  lightsGroup,
3167
3346
  center,
3168
3347
  radius,
3169
- // 新增灯光强度调整
3348
+ // Update light intensity
3170
3349
  updateLightIntensity(factor) {
3171
3350
  lightsGroup.traverse((node) => {
3172
3351
  if (node.isLight) {
@@ -3178,10 +3357,10 @@ function autoSetupCameraAndLight(camera, scene, model, options = {}) {
3178
3357
  },
3179
3358
  dispose: () => {
3180
3359
  try {
3181
- // 移除灯光组
3360
+ // Remove lights group
3182
3361
  if (lightsGroup.parent)
3183
3362
  lightsGroup.parent.remove(lightsGroup);
3184
- // 清理阴影资源
3363
+ // Dispose shadow resources
3185
3364
  lightsGroup.traverse((node) => {
3186
3365
  if (node.isLight) {
3187
3366
  const l = node;