@chocozhang/three-model-render 1.0.3 → 1.0.5

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 (43) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +134 -97
  3. package/dist/camera/index.d.ts +59 -36
  4. package/dist/camera/index.js +83 -67
  5. package/dist/camera/index.js.map +1 -1
  6. package/dist/camera/index.mjs +83 -67
  7. package/dist/camera/index.mjs.map +1 -1
  8. package/dist/core/index.d.ts +81 -28
  9. package/dist/core/index.js +194 -104
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/index.mjs +194 -105
  12. package/dist/core/index.mjs.map +1 -1
  13. package/dist/effect/index.d.ts +47 -134
  14. package/dist/effect/index.js +287 -288
  15. package/dist/effect/index.js.map +1 -1
  16. package/dist/effect/index.mjs +287 -288
  17. package/dist/effect/index.mjs.map +1 -1
  18. package/dist/index.d.ts +432 -349
  19. package/dist/index.js +1399 -1228
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +1395 -1229
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/interaction/index.d.ts +85 -52
  24. package/dist/interaction/index.js +168 -142
  25. package/dist/interaction/index.js.map +1 -1
  26. package/dist/interaction/index.mjs +168 -142
  27. package/dist/interaction/index.mjs.map +1 -1
  28. package/dist/loader/index.d.ts +106 -58
  29. package/dist/loader/index.js +492 -454
  30. package/dist/loader/index.js.map +1 -1
  31. package/dist/loader/index.mjs +491 -455
  32. package/dist/loader/index.mjs.map +1 -1
  33. package/dist/setup/index.d.ts +26 -24
  34. package/dist/setup/index.js +125 -163
  35. package/dist/setup/index.js.map +1 -1
  36. package/dist/setup/index.mjs +124 -164
  37. package/dist/setup/index.mjs.map +1 -1
  38. package/dist/ui/index.d.ts +18 -7
  39. package/dist/ui/index.js +45 -37
  40. package/dist/ui/index.js.map +1 -1
  41. package/dist/ui/index.mjs +45 -37
  42. package/dist/ui/index.mjs.map +1 -1
  43. package/package.json +50 -22
@@ -2,28 +2,38 @@ import * as THREE from 'three';
2
2
  import * as BufferGeometryUtils from 'three/examples/jsm/utils/BufferGeometryUtils.js';
3
3
 
4
4
  /**
5
- * 创建模型点击高亮工具(OutlinePass 版)- 优化版
5
+ * @file clickHandler.ts
6
+ * @description
7
+ * Tool for handling model clicks and highlighting (OutlinePass version).
6
8
  *
7
- * ✨ 功能增强:
8
- * - 使用 AbortController 统一管理事件生命周期
9
- * - 支持防抖处理避免频繁触发
10
- * - 可自定义 Raycaster 参数
11
- * - 根据相机距离动态调整描边厚度
9
+ * @best-practice
10
+ * - Use `createModelClickHandler` to setup interaction.
11
+ * - Handles debouncing and click threshold automatically.
12
+ * - Cleanup using the returned dispose function.
13
+ */
14
+ /**
15
+ * Create Model Click Highlight Tool (OutlinePass Version) - Optimized
16
+ *
17
+ * Features:
18
+ * - Uses AbortController to unify event lifecycle management
19
+ * - Supports debounce to avoid frequent triggering
20
+ * - Customizable Raycaster parameters
21
+ * - Dynamically adjusts outline thickness based on camera distance
12
22
  *
13
- * @param camera 相机
14
- * @param scene 场景
15
- * @param renderer 渲染器
16
- * @param outlinePass 已初始化的 OutlinePass
17
- * @param onClick 点击回调
18
- * @param options 可选配置
19
- * @returns dispose 函数,用于清理事件和资源
23
+ * @param camera Camera
24
+ * @param scene Scene
25
+ * @param renderer Renderer
26
+ * @param outlinePass Initialized OutlinePass
27
+ * @param onClick Click callback
28
+ * @param options Optional configuration
29
+ * @returns Dispose function, used to clean up events and resources
20
30
  */
21
31
  function createModelClickHandler(camera, scene, renderer, outlinePass, onClick, options = {}) {
22
- // 配置项
32
+ // Configuration
23
33
  const { clickThreshold = 3, debounceDelay = 0, raycasterParams = {}, enableDynamicThickness = true, minThickness = 1, maxThickness = 10 } = options;
24
34
  const raycaster = new THREE.Raycaster();
25
35
  const mouse = new THREE.Vector2();
26
- // 应用 raycaster 自定义参数
36
+ // Apply Raycaster custom parameters
27
37
  if (raycasterParams.near !== undefined)
28
38
  raycaster.near = raycasterParams.near;
29
39
  if (raycasterParams.far !== undefined)
@@ -40,25 +50,25 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
40
50
  let startY = 0;
41
51
  let selectedObject = null;
42
52
  let debounceTimer = null;
43
- // 使用 AbortController 统一管理事件
53
+ // Use AbortController to manage events uniformly
44
54
  const abortController = new AbortController();
45
55
  const signal = abortController.signal;
46
- /** 恢复对象高亮(清空 OutlinePass.selectedObjects */
56
+ /** Restore object highlight (Clear OutlinePass.selectedObjects) */
47
57
  function restoreObject() {
48
58
  outlinePass.selectedObjects = [];
49
59
  }
50
- /** 鼠标按下记录位置 */
60
+ /** Record mouse down position */
51
61
  function handleMouseDown(event) {
52
62
  startX = event.clientX;
53
63
  startY = event.clientY;
54
64
  }
55
- /** 鼠标抬起判定点击或拖动(带防抖) */
65
+ /** Mouse up determines click or drag (with debounce) */
56
66
  function handleMouseUp(event) {
57
67
  const dx = Math.abs(event.clientX - startX);
58
68
  const dy = Math.abs(event.clientY - startY);
59
69
  if (dx > clickThreshold || dy > clickThreshold)
60
- return; // 拖动不触发点击
61
- // 防抖处理
70
+ return; // Drag does not trigger click
71
+ // Debounce processing
62
72
  if (debounceDelay > 0) {
63
73
  if (debounceTimer !== null) {
64
74
  clearTimeout(debounceTimer);
@@ -72,7 +82,7 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
72
82
  processClick(event);
73
83
  }
74
84
  }
75
- /** 实际的点击处理逻辑 */
85
+ /** Actual click processing logic */
76
86
  function processClick(event) {
77
87
  const rect = renderer.domElement.getBoundingClientRect();
78
88
  mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
@@ -81,59 +91,67 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
81
91
  const intersects = raycaster.intersectObjects(scene.children, true);
82
92
  if (intersects.length > 0) {
83
93
  let object = intersects[0].object;
84
- // 点击不同模型,先清除之前高亮
94
+ // Click different model, clear previous highlight first
85
95
  if (selectedObject && selectedObject !== object)
86
96
  restoreObject();
87
97
  selectedObject = object;
88
- // highlightObject(selectedObject); // 可选:是否自动高亮
98
+ // highlightObject(selectedObject); // Optional: whether to auto highlight
89
99
  onClick(selectedObject, {
90
- name: selectedObject.name || '未命名模型',
100
+ name: selectedObject.name || 'Unnamed Model',
91
101
  position: selectedObject.getWorldPosition(new THREE.Vector3()),
92
102
  uuid: selectedObject.uuid
93
103
  });
94
104
  }
95
105
  else {
96
- // 点击空白 清除高亮
106
+ // Click blank -> Clear highlight
97
107
  if (selectedObject)
98
108
  restoreObject();
99
109
  selectedObject = null;
100
110
  onClick(null);
101
111
  }
102
112
  }
103
- // 使用 AbortController signal 注册事件
113
+ // Register events using signal from AbortController
104
114
  renderer.domElement.addEventListener('mousedown', handleMouseDown, { signal });
105
115
  renderer.domElement.addEventListener('mouseup', handleMouseUp, { signal });
106
- /** 销毁函数:解绑事件并清除高亮 */
116
+ /** Dispose function: Unbind events and clear highlight */
107
117
  return () => {
108
- // 清理防抖定时器
118
+ // Clear debounce timer
109
119
  if (debounceTimer !== null) {
110
120
  clearTimeout(debounceTimer);
111
121
  debounceTimer = null;
112
122
  }
113
- // 一次性解绑所有事件
123
+ // Unbind all events at once
114
124
  abortController.abort();
115
- // 清除高亮
125
+ // Clear highlight
116
126
  restoreObject();
117
- // 清空引用
127
+ // Clear reference
118
128
  selectedObject = null;
119
129
  };
120
130
  }
121
131
 
122
- // src/utils/ArrowGuide.ts
123
132
  /**
124
- * ArrowGuide - 优化版
125
- * 箭头引导效果工具,支持高亮模型并淡化其他对象
133
+ * @file arrowGuide.ts
134
+ * @description
135
+ * Arrow guide effect tool, supports highlighting models and fading other objects.
136
+ *
137
+ * @best-practice
138
+ * - Use `highlight` to focus on specific models.
139
+ * - Automatically manages materials and memory using WeakMap.
140
+ * - Call `dispose` when component unmounts.
141
+ */
142
+ /**
143
+ * ArrowGuide - Optimized Version
144
+ * Arrow guide effect tool, supports highlighting models and fading other objects.
126
145
  *
127
- * ✨ 优化内容:
128
- * - 使用 WeakMap 自动回收材质,避免内存泄漏
129
- * - 使用 AbortController 管理事件生命周期
130
- * - 添加材质复用机制,减少重复创建
131
- * - 改进 dispose 逻辑,确保完全释放资源
132
- * - 添加错误处理和边界检查
146
+ * Features:
147
+ * - Uses WeakMap for automatic material recycling, preventing memory leaks
148
+ * - Uses AbortController to manage event lifecycle
149
+ * - Adds material reuse mechanism to reuse materials
150
+ * - Improved dispose logic ensuring complete resource release
151
+ * - Adds error handling and boundary checks
133
152
  */
134
153
  class ArrowGuide {
135
154
  constructor(renderer, camera, scene, options) {
136
- var _a, _b, _c;
137
155
  this.renderer = renderer;
138
156
  this.camera = camera;
139
157
  this.scene = scene;
@@ -144,45 +162,45 @@ class ArrowGuide {
144
162
  this.clickThreshold = 10;
145
163
  this.raycaster = new THREE.Raycaster();
146
164
  this.mouse = new THREE.Vector2();
147
- // 使用 WeakMap 自动回收材质(GC 友好)
165
+ // Use WeakMap for automatic material recycling (GC friendly)
148
166
  this.originalMaterials = new WeakMap();
149
167
  this.fadedMaterials = new WeakMap();
150
- // AbortController 用于事件管理
168
+ // AbortController for event management
151
169
  this.abortController = null;
152
- // 配置:非高亮透明度和亮度
170
+ // Config: Non-highlight opacity and brightness
153
171
  this.fadeOpacity = 0.5;
154
172
  this.fadeBrightness = 0.1;
155
- this.clickThreshold = (_a = options === null || options === void 0 ? void 0 : options.clickThreshold) !== null && _a !== void 0 ? _a : 10;
156
- this.ignoreRaycastNames = new Set((options === null || options === void 0 ? void 0 : options.ignoreRaycastNames) || []);
157
- this.fadeOpacity = (_b = options === null || options === void 0 ? void 0 : options.fadeOpacity) !== null && _b !== void 0 ? _b : 0.5;
158
- this.fadeBrightness = (_c = options === null || options === void 0 ? void 0 : options.fadeBrightness) !== null && _c !== void 0 ? _c : 0.1;
173
+ this.clickThreshold = options?.clickThreshold ?? 10;
174
+ this.ignoreRaycastNames = new Set(options?.ignoreRaycastNames || []);
175
+ this.fadeOpacity = options?.fadeOpacity ?? 0.5;
176
+ this.fadeBrightness = options?.fadeBrightness ?? 0.1;
159
177
  this.abortController = new AbortController();
160
178
  this.initEvents();
161
179
  }
162
- // —— 工具:缓存原材质(仅首次)
180
+ // Tool: Cache original material (first time only)
163
181
  cacheOriginalMaterial(mesh) {
164
182
  if (!this.originalMaterials.has(mesh)) {
165
183
  this.originalMaterials.set(mesh, mesh.material);
166
184
  }
167
185
  }
168
- // —— 工具:为某个材质克隆一个"半透明版本",保留所有贴图与参数
186
+ // Tool: Clone a "translucent version" for a material, preserving all maps and parameters
169
187
  makeFadedClone(mat) {
170
188
  const clone = mat.clone();
171
189
  const c = clone;
172
- // 只改透明相关参数,不改 map / normalMap / roughnessMap 等细节
190
+ // Only modify transparency parameters, do not modify detail maps like map / normalMap / roughnessMap
173
191
  c.transparent = true;
174
192
  if (typeof c.opacity === 'number')
175
193
  c.opacity = this.fadeOpacity;
176
194
  if (c.color && c.color.isColor) {
177
- c.color.multiplyScalar(this.fadeBrightness); // 颜色整体变暗
195
+ c.color.multiplyScalar(this.fadeBrightness); // Darken color overall
178
196
  }
179
- // 为了让箭头在透明建筑后也能顺畅显示,常用策略:不写深度,仅测试深度
197
+ // Common strategy for fluid display behind transparent objects: do not write depth, only test depth
180
198
  clone.depthWrite = false;
181
199
  clone.depthTest = true;
182
200
  clone.needsUpdate = true;
183
201
  return clone;
184
202
  }
185
- // —— 工具:为 mesh.material(可能是数组)批量克隆"半透明版本"
203
+ // Tool: Batch clone "translucent version" for mesh.material (could be array)
186
204
  createFadedMaterialFrom(mesh) {
187
205
  const orig = mesh.material;
188
206
  if (Array.isArray(orig)) {
@@ -191,7 +209,7 @@ class ArrowGuide {
191
209
  return this.makeFadedClone(orig);
192
210
  }
193
211
  /**
194
- * 设置箭头 Mesh
212
+ * Set Arrow Mesh
195
213
  */
196
214
  setArrowMesh(mesh) {
197
215
  this.lxMesh = mesh;
@@ -207,15 +225,15 @@ class ArrowGuide {
207
225
  mesh.visible = false;
208
226
  }
209
227
  catch (error) {
210
- console.error('ArrowGuide: 设置箭头材质失败', error);
228
+ console.error('ArrowGuide: Failed to set arrow material', error);
211
229
  }
212
230
  }
213
231
  /**
214
- * 高亮指定模型
232
+ * Highlight specified models
215
233
  */
216
234
  highlight(models) {
217
235
  if (!models || models.length === 0) {
218
- console.warn('ArrowGuide: 高亮模型列表为空');
236
+ console.warn('ArrowGuide: Highlight model list is empty');
219
237
  return;
220
238
  }
221
239
  this.modelBrightArr = models;
@@ -224,9 +242,9 @@ class ArrowGuide {
224
242
  this.lxMesh.visible = true;
225
243
  this.applyHighlight();
226
244
  }
227
- // 应用高亮效果:非高亮模型保留细节 使用"克隆后的半透明材质"
245
+ // Apply highlight effect: Non-highlighted models preserve details -> use "cloned translucent material"
228
246
  applyHighlight() {
229
- // 使用 Set 提升查找性能
247
+ // Use Set to improve lookup performance
230
248
  const keepMeshes = new Set();
231
249
  this.modelBrightArr.forEach(obj => {
232
250
  obj.traverse(child => {
@@ -238,21 +256,21 @@ class ArrowGuide {
238
256
  this.scene.traverse(obj => {
239
257
  if (obj.isMesh) {
240
258
  const mesh = obj;
241
- // 缓存原材质(用于恢复)
259
+ // Cache original material (for restoration)
242
260
  this.cacheOriginalMaterial(mesh);
243
261
  if (!keepMeshes.has(mesh)) {
244
- // 非高亮:如果还没给它生成过"半透明克隆材质",就创建一次
262
+ // Non-highlighted: if no "translucent clone material" generated yet, create one
245
263
  if (!this.fadedMaterials.has(mesh)) {
246
264
  const faded = this.createFadedMaterialFrom(mesh);
247
265
  this.fadedMaterials.set(mesh, faded);
248
266
  }
249
- // 替换为克隆材质(保留所有贴图/法线等细节)
267
+ // Replace with clone material (preserve all maps/normals details)
250
268
  const fadedMat = this.fadedMaterials.get(mesh);
251
269
  if (fadedMat)
252
270
  mesh.material = fadedMat;
253
271
  }
254
272
  else {
255
- // 高亮对象:确保回到原材质(避免上一次高亮后遗留)
273
+ // Highlighted object: ensure return to original material (avoid leftover from previous highlight)
256
274
  const orig = this.originalMaterials.get(mesh);
257
275
  if (orig && mesh.material !== orig) {
258
276
  mesh.material = orig;
@@ -263,16 +281,16 @@ class ArrowGuide {
263
281
  });
264
282
  }
265
283
  catch (error) {
266
- console.error('ArrowGuide: 应用高亮失败', error);
284
+ console.error('ArrowGuide: Failed to apply highlight', error);
267
285
  }
268
286
  }
269
- // 恢复为原材质 & 释放克隆材质
287
+ // Restore to original material & dispose clone material
270
288
  restore() {
271
289
  this.flowActive = false;
272
290
  if (this.lxMesh)
273
291
  this.lxMesh.visible = false;
274
292
  try {
275
- // 收集所有需要释放的材质
293
+ // Collect all materials to dispose
276
294
  const materialsToDispose = [];
277
295
  this.scene.traverse(obj => {
278
296
  if (obj.isMesh) {
@@ -282,7 +300,7 @@ class ArrowGuide {
282
300
  mesh.material = orig;
283
301
  mesh.material.needsUpdate = true;
284
302
  }
285
- // 收集待释放的淡化材质
303
+ // Collect faded materials to dispose
286
304
  const faded = this.fadedMaterials.get(mesh);
287
305
  if (faded) {
288
306
  if (Array.isArray(faded)) {
@@ -294,24 +312,24 @@ class ArrowGuide {
294
312
  }
295
313
  }
296
314
  });
297
- // 批量释放材质(不触碰贴图资源)
315
+ // Batch dispose materials (do not touch texture resources)
298
316
  materialsToDispose.forEach(mat => {
299
317
  try {
300
318
  mat.dispose();
301
319
  }
302
320
  catch (error) {
303
- console.error('ArrowGuide: 释放材质失败', error);
321
+ console.error('ArrowGuide: Failed to dispose material', error);
304
322
  }
305
323
  });
306
- // 创建新的 WeakMap(相当于清空)
324
+ // Create new WeakMap (equivalent to clearing)
307
325
  this.fadedMaterials = new WeakMap();
308
326
  }
309
327
  catch (error) {
310
- console.error('ArrowGuide: 恢复材质失败', error);
328
+ console.error('ArrowGuide: Failed to restore material', error);
311
329
  }
312
330
  }
313
331
  /**
314
- * 动画更新(每帧调用)
332
+ * Animation update (called every frame)
315
333
  */
316
334
  animate() {
317
335
  if (!this.flowActive || !this.lxMesh)
@@ -325,16 +343,16 @@ class ArrowGuide {
325
343
  }
326
344
  }
327
345
  catch (error) {
328
- console.error('ArrowGuide: 动画更新失败', error);
346
+ console.error('ArrowGuide: Animation update failed', error);
329
347
  }
330
348
  }
331
349
  /**
332
- * 初始化事件监听器
350
+ * Initialize event listeners
333
351
  */
334
352
  initEvents() {
335
353
  const dom = this.renderer.domElement;
336
354
  const signal = this.abortController.signal;
337
- // 使用 AbortController signal 自动管理事件生命周期
355
+ // Use AbortController signal to automatically manage event lifecycle
338
356
  dom.addEventListener('pointerdown', (e) => {
339
357
  this.pointerDownPos.set(e.clientX, e.clientY);
340
358
  }, { signal });
@@ -342,7 +360,7 @@ class ArrowGuide {
342
360
  const dx = Math.abs(e.clientX - this.pointerDownPos.x);
343
361
  const dy = Math.abs(e.clientY - this.pointerDownPos.y);
344
362
  if (dx > this.clickThreshold || dy > this.clickThreshold)
345
- return; // 拖拽
363
+ return; // Dragging
346
364
  const rect = dom.getBoundingClientRect();
347
365
  this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
348
366
  this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
@@ -356,21 +374,21 @@ class ArrowGuide {
356
374
  return true;
357
375
  });
358
376
  if (filtered.length === 0)
359
- this.restore(); // 点击空白恢复
377
+ this.restore(); // Click blank space to restore
360
378
  }, { signal });
361
379
  }
362
380
  /**
363
- * 释放所有资源
381
+ * Dispose all resources
364
382
  */
365
383
  dispose() {
366
- // 先恢复材质
384
+ // Restore materials first
367
385
  this.restore();
368
- // 使用 AbortController 一次性解绑所有事件
386
+ // Unbind all events at once using AbortController
369
387
  if (this.abortController) {
370
388
  this.abortController.abort();
371
389
  this.abortController = null;
372
390
  }
373
- // 清空引用
391
+ // Clear references
374
392
  this.modelBrightArr = [];
375
393
  this.lxMesh = null;
376
394
  this.fadedMaterials = new WeakMap();
@@ -379,50 +397,59 @@ class ArrowGuide {
379
397
  }
380
398
  }
381
399
 
382
- // utils/LiquidFillerGroup.ts
383
400
  /**
384
- * LiquidFillerGroup - 优化版
385
- * 支持单模型或多模型液位动画、独立颜色控制
401
+ * @file liquidFiller.ts
402
+ * @description
403
+ * Liquid filling effect for single or multiple models using local clipping planes.
386
404
  *
387
- * ✨ 优化内容:
388
- * - 使用 renderer.domElement 替代 window 事件
389
- * - 使用 AbortController 管理事件生命周期
390
- * - 添加错误处理和边界检查
391
- * - 优化动画管理,避免内存泄漏
392
- * - 完善资源释放逻辑
405
+ * @best-practice
406
+ * - Use `fillTo` to animate liquid level.
407
+ * - Supports multiple independent liquid levels.
408
+ * - Call `dispose` to clean up resources and event listeners.
409
+ */
410
+ /**
411
+ * LiquidFillerGroup - Optimized
412
+ * Supports single or multi-model liquid level animation with independent color control.
413
+ *
414
+ * Features:
415
+ * - Uses renderer.domElement instead of window events
416
+ * - Uses AbortController to manage event lifecycle
417
+ * - Adds error handling and boundary checks
418
+ * - Optimized animation management to prevent memory leaks
419
+ * - Comprehensive resource disposal logic
393
420
  */
394
421
  class LiquidFillerGroup {
395
422
  /**
396
- * 构造函数
397
- * @param models 单个或多个 THREE.Object3D
398
- * @param scene 场景
399
- * @param camera 相机
400
- * @param renderer 渲染器
401
- * @param defaultOptions 默认液体选项
402
- * @param clickThreshold 点击阈值,单位像素
423
+ * Constructor
424
+ * @param models Single or multiple THREE.Object3D
425
+ * @param scene Scene
426
+ * @param camera Camera
427
+ * @param renderer Renderer
428
+ * @param defaultOptions Default liquid options
429
+ * @param clickThreshold Click threshold in pixels
403
430
  */
404
431
  constructor(models, scene, camera, renderer, defaultOptions, clickThreshold = 10) {
405
432
  this.items = [];
406
433
  this.raycaster = new THREE.Raycaster();
407
434
  this.pointerDownPos = new THREE.Vector2();
408
435
  this.clickThreshold = 10;
409
- this.abortController = null; // 事件管理器
410
- /** pointerdown 记录位置 */
436
+ this.abortController = null; // Event manager
437
+ /** pointerdown record position */
411
438
  this.handlePointerDown = (event) => {
412
439
  this.pointerDownPos.set(event.clientX, event.clientY);
413
440
  };
414
- /** pointerup 判断点击空白,恢复原始材质 */
441
+ /** pointerup check click blank, restore original material */
415
442
  this.handlePointerUp = (event) => {
416
443
  const dx = event.clientX - this.pointerDownPos.x;
417
444
  const dy = event.clientY - this.pointerDownPos.y;
418
445
  const distance = Math.sqrt(dx * dx + dy * dy);
419
446
  if (distance > this.clickThreshold)
420
- return; // 拖拽不触发
421
- // 使用 renderer.domElement 的实际尺寸
447
+ return; // Do not trigger on drag
448
+ // Use renderer.domElement actual size
422
449
  const rect = this.renderer.domElement.getBoundingClientRect();
423
450
  const pointerNDC = new THREE.Vector2(((event.clientX - rect.left) / rect.width) * 2 - 1, -((event.clientY - rect.top) / rect.height) * 2 + 1);
424
451
  this.raycaster.setFromCamera(pointerNDC, this.camera);
425
- // 点击空白 -> 所有模型恢复
452
+ // Click blank -> Restore all models
426
453
  const intersectsAny = this.items.some(item => this.raycaster.intersectObject(item.model, true).length > 0);
427
454
  if (!intersectsAny) {
428
455
  this.restoreAll();
@@ -432,18 +459,17 @@ class LiquidFillerGroup {
432
459
  this.camera = camera;
433
460
  this.renderer = renderer;
434
461
  this.clickThreshold = clickThreshold;
435
- // 创建 AbortController 用于事件管理
462
+ // Create AbortController for event management
436
463
  this.abortController = new AbortController();
437
464
  const modelArray = Array.isArray(models) ? models : [models];
438
465
  modelArray.forEach(model => {
439
- var _a, _b, _c;
440
466
  try {
441
467
  const options = {
442
- color: (_a = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.color) !== null && _a !== void 0 ? _a : 0x00ff00,
443
- opacity: (_b = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.opacity) !== null && _b !== void 0 ? _b : 0.6,
444
- speed: (_c = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.speed) !== null && _c !== void 0 ? _c : 0.05,
468
+ color: defaultOptions?.color ?? 0x00ff00,
469
+ opacity: defaultOptions?.opacity ?? 0.6,
470
+ speed: defaultOptions?.speed ?? 0.05,
445
471
  };
446
- // 保存原始材质
472
+ // Save original materials
447
473
  const originalMaterials = new Map();
448
474
  model.traverse(obj => {
449
475
  if (obj.isMesh) {
@@ -451,12 +477,12 @@ class LiquidFillerGroup {
451
477
  originalMaterials.set(mesh, mesh.material);
452
478
  }
453
479
  });
454
- // 边界检查:确保有材质可以保存
480
+ // Boundary check: ensure there are materials to save
455
481
  if (originalMaterials.size === 0) {
456
- console.warn('LiquidFillerGroup: 模型没有 Mesh 对象', model);
482
+ console.warn('LiquidFillerGroup: Model has no Mesh objects', model);
457
483
  return;
458
484
  }
459
- // 应用淡线框材质
485
+ // Apply faded wireframe material
460
486
  model.traverse(obj => {
461
487
  if (obj.isMesh) {
462
488
  const mesh = obj;
@@ -468,7 +494,7 @@ class LiquidFillerGroup {
468
494
  });
469
495
  }
470
496
  });
471
- // 创建液体 Mesh
497
+ // Create liquid Mesh
472
498
  const geometries = [];
473
499
  model.traverse(obj => {
474
500
  if (obj.isMesh) {
@@ -479,12 +505,12 @@ class LiquidFillerGroup {
479
505
  }
480
506
  });
481
507
  if (geometries.length === 0) {
482
- console.warn('LiquidFillerGroup: 模型没有几何体', model);
508
+ console.warn('LiquidFillerGroup: Model has no geometries', model);
483
509
  return;
484
510
  }
485
511
  const mergedGeometry = BufferGeometryUtils.mergeGeometries(geometries, false);
486
512
  if (!mergedGeometry) {
487
- console.error('LiquidFillerGroup: 几何体合并失败', model);
513
+ console.error('LiquidFillerGroup: Failed to merge geometries', model);
488
514
  return;
489
515
  }
490
516
  const material = new THREE.MeshPhongMaterial({
@@ -495,7 +521,7 @@ class LiquidFillerGroup {
495
521
  });
496
522
  const liquidMesh = new THREE.Mesh(mergedGeometry, material);
497
523
  this.scene.add(liquidMesh);
498
- // 设置 clippingPlane
524
+ // Set clippingPlane
499
525
  const clipPlane = new THREE.Plane(new THREE.Vector3(0, -1, 0), 0);
500
526
  const mat = liquidMesh.material;
501
527
  mat.clippingPlanes = [clipPlane];
@@ -506,41 +532,41 @@ class LiquidFillerGroup {
506
532
  clipPlane,
507
533
  originalMaterials,
508
534
  options,
509
- animationId: null // 初始化动画 ID
535
+ animationId: null // Initialize animation ID
510
536
  });
511
537
  }
512
538
  catch (error) {
513
- console.error('LiquidFillerGroup: 初始化模型失败', model, error);
539
+ console.error('LiquidFillerGroup: Failed to initialize model', model, error);
514
540
  }
515
541
  });
516
- // 使用 renderer.domElement 替代 window,使用 AbortController signal
542
+ // Use renderer.domElement instead of window, use AbortController signal
517
543
  const signal = this.abortController.signal;
518
544
  this.renderer.domElement.addEventListener('pointerdown', this.handlePointerDown, { signal });
519
545
  this.renderer.domElement.addEventListener('pointerup', this.handlePointerUp, { signal });
520
546
  }
521
547
  /**
522
- * 设置液位
523
- * @param models 单个模型或模型数组
524
- * @param percent 液位百分比 0~1
525
- */
548
+ * Set liquid level
549
+ * @param models Single model or array of models
550
+ * @param percent Liquid level percentage 0~1
551
+ */
526
552
  fillTo(models, percent) {
527
- // 边界检查
553
+ // Boundary check
528
554
  if (percent < 0 || percent > 1) {
529
- console.warn('LiquidFillerGroup: percent 必须在 0~1 之间', percent);
555
+ console.warn('LiquidFillerGroup: percent must be between 0 and 1', percent);
530
556
  percent = Math.max(0, Math.min(1, percent));
531
557
  }
532
558
  const modelArray = Array.isArray(models) ? models : [models];
533
559
  modelArray.forEach(model => {
534
560
  const item = this.items.find(i => i.model === model);
535
561
  if (!item) {
536
- console.warn('LiquidFillerGroup: 未找到模型', model);
562
+ console.warn('LiquidFillerGroup: Model not found', model);
537
563
  return;
538
564
  }
539
565
  if (!item.liquidMesh) {
540
- console.warn('LiquidFillerGroup: liquidMesh 已被释放', model);
566
+ console.warn('LiquidFillerGroup: liquidMesh already disposed', model);
541
567
  return;
542
568
  }
543
- // 取消之前的动画
569
+ // Cancel previous animation
544
570
  if (item.animationId !== null) {
545
571
  cancelAnimationFrame(item.animationId);
546
572
  item.animationId = null;
@@ -568,14 +594,14 @@ class LiquidFillerGroup {
568
594
  animate();
569
595
  }
570
596
  catch (error) {
571
- console.error('LiquidFillerGroup: fillTo 执行失败', model, error);
597
+ console.error('LiquidFillerGroup: fillTo execution failed', model, error);
572
598
  }
573
599
  });
574
600
  }
575
- /** 设置多个模型液位,percentList items 顺序对应 */
601
+ /** Set multiple model levels, percentList corresponds to items order */
576
602
  fillToAll(percentList) {
577
603
  if (percentList.length !== this.items.length) {
578
- console.warn(`LiquidFillerGroup: percentList 长度 (${percentList.length}) items 长度 (${this.items.length}) 不匹配`);
604
+ console.warn(`LiquidFillerGroup: percentList length (${percentList.length}) does not match items length (${this.items.length})`);
579
605
  }
580
606
  percentList.forEach((p, idx) => {
581
607
  if (idx < this.items.length) {
@@ -583,17 +609,17 @@ class LiquidFillerGroup {
583
609
  }
584
610
  });
585
611
  }
586
- /** 恢复单个模型原始材质并移除液体 */
612
+ /** Restore single model original material and remove liquid */
587
613
  restore(model) {
588
614
  const item = this.items.find(i => i.model === model);
589
615
  if (!item)
590
616
  return;
591
- // 取消动画
617
+ // Cancel animation
592
618
  if (item.animationId !== null) {
593
619
  cancelAnimationFrame(item.animationId);
594
620
  item.animationId = null;
595
621
  }
596
- // 恢复原始材质
622
+ // Restore original material
597
623
  item.model.traverse(obj => {
598
624
  if (obj.isMesh) {
599
625
  const mesh = obj;
@@ -602,7 +628,7 @@ class LiquidFillerGroup {
602
628
  mesh.material = original;
603
629
  }
604
630
  });
605
- // 释放液体 Mesh
631
+ // Dispose liquid Mesh
606
632
  if (item.liquidMesh) {
607
633
  this.scene.remove(item.liquidMesh);
608
634
  item.liquidMesh.geometry.dispose();
@@ -615,20 +641,20 @@ class LiquidFillerGroup {
615
641
  item.liquidMesh = null;
616
642
  }
617
643
  }
618
- /** 恢复所有模型 */
644
+ /** Restore all models */
619
645
  restoreAll() {
620
646
  this.items.forEach(item => this.restore(item.model));
621
647
  }
622
- /** 销毁方法,释放事件和资源 */
648
+ /** Dispose method, release events and resources */
623
649
  dispose() {
624
- // 先恢复所有模型
650
+ // Restore all models first
625
651
  this.restoreAll();
626
- // 使用 AbortController 一次性解绑所有事件
652
+ // Unbind all events at once using AbortController
627
653
  if (this.abortController) {
628
654
  this.abortController.abort();
629
655
  this.abortController = null;
630
656
  }
631
- // 清空 items
657
+ // Clear items
632
658
  this.items.length = 0;
633
659
  }
634
660
  }