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