@chocozhang/three-model-render 1.0.1

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/LICENSE +21 -0
  2. package/README.md +609 -0
  3. package/dist/camera/index.d.ts +133 -0
  4. package/dist/camera/index.js +291 -0
  5. package/dist/camera/index.js.map +1 -0
  6. package/dist/camera/index.mjs +265 -0
  7. package/dist/camera/index.mjs.map +1 -0
  8. package/dist/core/index.d.ts +102 -0
  9. package/dist/core/index.js +455 -0
  10. package/dist/core/index.js.map +1 -0
  11. package/dist/core/index.mjs +432 -0
  12. package/dist/core/index.mjs.map +1 -0
  13. package/dist/effect/index.d.ts +214 -0
  14. package/dist/effect/index.js +749 -0
  15. package/dist/effect/index.js.map +1 -0
  16. package/dist/effect/index.mjs +728 -0
  17. package/dist/effect/index.mjs.map +1 -0
  18. package/dist/index.d.ts +852 -0
  19. package/dist/index.js +3268 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/index.mjs +3223 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/interaction/index.d.ts +160 -0
  24. package/dist/interaction/index.js +661 -0
  25. package/dist/interaction/index.js.map +1 -0
  26. package/dist/interaction/index.mjs +637 -0
  27. package/dist/interaction/index.mjs.map +1 -0
  28. package/dist/loader/index.d.ts +175 -0
  29. package/dist/loader/index.js +786 -0
  30. package/dist/loader/index.js.map +1 -0
  31. package/dist/loader/index.mjs +758 -0
  32. package/dist/loader/index.mjs.map +1 -0
  33. package/dist/setup/index.d.ts +47 -0
  34. package/dist/setup/index.js +199 -0
  35. package/dist/setup/index.js.map +1 -0
  36. package/dist/setup/index.mjs +178 -0
  37. package/dist/setup/index.mjs.map +1 -0
  38. package/dist/ui/index.d.ts +36 -0
  39. package/dist/ui/index.js +292 -0
  40. package/dist/ui/index.js.map +1 -0
  41. package/dist/ui/index.mjs +271 -0
  42. package/dist/ui/index.mjs.map +1 -0
  43. package/package.json +98 -0
@@ -0,0 +1,661 @@
1
+ 'use strict';
2
+
3
+ var THREE = require('three');
4
+ var BufferGeometryUtils = require('three/examples/jsm/utils/BufferGeometryUtils.js');
5
+
6
+ function _interopNamespaceDefault(e) {
7
+ var n = Object.create(null);
8
+ if (e) {
9
+ Object.keys(e).forEach(function (k) {
10
+ if (k !== 'default') {
11
+ var d = Object.getOwnPropertyDescriptor(e, k);
12
+ Object.defineProperty(n, k, d.get ? d : {
13
+ enumerable: true,
14
+ get: function () { return e[k]; }
15
+ });
16
+ }
17
+ });
18
+ }
19
+ n.default = e;
20
+ return Object.freeze(n);
21
+ }
22
+
23
+ var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
24
+ var BufferGeometryUtils__namespace = /*#__PURE__*/_interopNamespaceDefault(BufferGeometryUtils);
25
+
26
+ /**
27
+ * 创建模型点击高亮工具(OutlinePass 版)- 优化版
28
+ *
29
+ * ✨ 功能增强:
30
+ * - 使用 AbortController 统一管理事件生命周期
31
+ * - 支持防抖处理避免频繁触发
32
+ * - 可自定义 Raycaster 参数
33
+ * - 根据相机距离动态调整描边厚度
34
+ *
35
+ * @param camera 相机
36
+ * @param scene 场景
37
+ * @param renderer 渲染器
38
+ * @param outlinePass 已初始化的 OutlinePass
39
+ * @param onClick 点击回调
40
+ * @param options 可选配置
41
+ * @returns dispose 函数,用于清理事件和资源
42
+ */
43
+ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick, options = {}) {
44
+ // 配置项
45
+ const { clickThreshold = 3, debounceDelay = 0, raycasterParams = {}, enableDynamicThickness = true, minThickness = 1, maxThickness = 10 } = options;
46
+ const raycaster = new THREE__namespace.Raycaster();
47
+ const mouse = new THREE__namespace.Vector2();
48
+ // 应用 raycaster 自定义参数
49
+ if (raycasterParams.near !== undefined)
50
+ raycaster.near = raycasterParams.near;
51
+ if (raycasterParams.far !== undefined)
52
+ raycaster.far = raycasterParams.far;
53
+ if (raycasterParams.pointsPrecision !== undefined) {
54
+ if (!raycaster.params.Points) {
55
+ raycaster.params.Points = { threshold: raycasterParams.pointsPrecision };
56
+ }
57
+ else {
58
+ raycaster.params.Points.threshold = raycasterParams.pointsPrecision;
59
+ }
60
+ }
61
+ let startX = 0;
62
+ let startY = 0;
63
+ let selectedObject = null;
64
+ let debounceTimer = null;
65
+ // 使用 AbortController 统一管理事件
66
+ const abortController = new AbortController();
67
+ const signal = abortController.signal;
68
+ /** 恢复对象高亮(清空 OutlinePass.selectedObjects) */
69
+ function restoreObject() {
70
+ outlinePass.selectedObjects = [];
71
+ }
72
+ /** 鼠标按下记录位置 */
73
+ function handleMouseDown(event) {
74
+ startX = event.clientX;
75
+ startY = event.clientY;
76
+ }
77
+ /** 鼠标抬起判定点击或拖动(带防抖) */
78
+ function handleMouseUp(event) {
79
+ const dx = Math.abs(event.clientX - startX);
80
+ const dy = Math.abs(event.clientY - startY);
81
+ if (dx > clickThreshold || dy > clickThreshold)
82
+ return; // 拖动不触发点击
83
+ // 防抖处理
84
+ if (debounceDelay > 0) {
85
+ if (debounceTimer !== null) {
86
+ clearTimeout(debounceTimer);
87
+ }
88
+ debounceTimer = window.setTimeout(() => {
89
+ processClick(event);
90
+ debounceTimer = null;
91
+ }, debounceDelay);
92
+ }
93
+ else {
94
+ processClick(event);
95
+ }
96
+ }
97
+ /** 实际的点击处理逻辑 */
98
+ function processClick(event) {
99
+ const rect = renderer.domElement.getBoundingClientRect();
100
+ mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
101
+ mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
102
+ raycaster.setFromCamera(mouse, camera);
103
+ const intersects = raycaster.intersectObjects(scene.children, true);
104
+ if (intersects.length > 0) {
105
+ let object = intersects[0].object;
106
+ // 点击不同模型,先清除之前高亮
107
+ if (selectedObject && selectedObject !== object)
108
+ restoreObject();
109
+ selectedObject = object;
110
+ // highlightObject(selectedObject); // 可选:是否自动高亮
111
+ onClick(selectedObject, {
112
+ name: selectedObject.name || '未命名模型',
113
+ position: selectedObject.getWorldPosition(new THREE__namespace.Vector3()),
114
+ uuid: selectedObject.uuid
115
+ });
116
+ }
117
+ else {
118
+ // 点击空白 → 清除高亮
119
+ if (selectedObject)
120
+ restoreObject();
121
+ selectedObject = null;
122
+ onClick(null);
123
+ }
124
+ }
125
+ // 使用 AbortController 的 signal 注册事件
126
+ renderer.domElement.addEventListener('mousedown', handleMouseDown, { signal });
127
+ renderer.domElement.addEventListener('mouseup', handleMouseUp, { signal });
128
+ /** 销毁函数:解绑事件并清除高亮 */
129
+ return () => {
130
+ // 清理防抖定时器
131
+ if (debounceTimer !== null) {
132
+ clearTimeout(debounceTimer);
133
+ debounceTimer = null;
134
+ }
135
+ // 一次性解绑所有事件
136
+ abortController.abort();
137
+ // 清除高亮
138
+ restoreObject();
139
+ // 清空引用
140
+ selectedObject = null;
141
+ };
142
+ }
143
+
144
+ // src/utils/ArrowGuide.ts
145
+ /**
146
+ * ArrowGuide - 优化版
147
+ * 箭头引导效果工具,支持高亮模型并淡化其他对象
148
+ *
149
+ * ✨ 优化内容:
150
+ * - 使用 WeakMap 自动回收材质,避免内存泄漏
151
+ * - 使用 AbortController 管理事件生命周期
152
+ * - 添加材质复用机制,减少重复创建
153
+ * - 改进 dispose 逻辑,确保完全释放资源
154
+ * - 添加错误处理和边界检查
155
+ */
156
+ class ArrowGuide {
157
+ constructor(renderer, camera, scene, options) {
158
+ var _a, _b, _c;
159
+ this.renderer = renderer;
160
+ this.camera = camera;
161
+ this.scene = scene;
162
+ this.lxMesh = null;
163
+ this.flowActive = false;
164
+ this.modelBrightArr = [];
165
+ this.pointerDownPos = new THREE__namespace.Vector2();
166
+ this.clickThreshold = 10;
167
+ this.raycaster = new THREE__namespace.Raycaster();
168
+ this.mouse = new THREE__namespace.Vector2();
169
+ // ✨ 使用 WeakMap 自动回收材质(GC 友好)
170
+ this.originalMaterials = new WeakMap();
171
+ this.fadedMaterials = new WeakMap();
172
+ // ✨ AbortController 用于事件管理
173
+ this.abortController = null;
174
+ // 配置:非高亮透明度和亮度
175
+ this.fadeOpacity = 0.5;
176
+ this.fadeBrightness = 0.1;
177
+ this.clickThreshold = (_a = options === null || options === void 0 ? void 0 : options.clickThreshold) !== null && _a !== void 0 ? _a : 10;
178
+ this.ignoreRaycastNames = new Set((options === null || options === void 0 ? void 0 : options.ignoreRaycastNames) || []);
179
+ this.fadeOpacity = (_b = options === null || options === void 0 ? void 0 : options.fadeOpacity) !== null && _b !== void 0 ? _b : 0.5;
180
+ this.fadeBrightness = (_c = options === null || options === void 0 ? void 0 : options.fadeBrightness) !== null && _c !== void 0 ? _c : 0.1;
181
+ this.abortController = new AbortController();
182
+ this.initEvents();
183
+ }
184
+ // —— 工具:缓存原材质(仅首次)
185
+ cacheOriginalMaterial(mesh) {
186
+ if (!this.originalMaterials.has(mesh)) {
187
+ this.originalMaterials.set(mesh, mesh.material);
188
+ }
189
+ }
190
+ // —— 工具:为某个材质克隆一个"半透明版本",保留所有贴图与参数
191
+ makeFadedClone(mat) {
192
+ const clone = mat.clone();
193
+ const c = clone;
194
+ // 只改透明相关参数,不改 map / normalMap / roughnessMap 等细节
195
+ c.transparent = true;
196
+ if (typeof c.opacity === 'number')
197
+ c.opacity = this.fadeOpacity;
198
+ if (c.color && c.color.isColor) {
199
+ c.color.multiplyScalar(this.fadeBrightness); // 颜色整体变暗
200
+ }
201
+ // 为了让箭头在透明建筑后也能顺畅显示,常用策略:不写深度,仅测试深度
202
+ clone.depthWrite = false;
203
+ clone.depthTest = true;
204
+ clone.needsUpdate = true;
205
+ return clone;
206
+ }
207
+ // —— 工具:为 mesh.material(可能是数组)批量克隆"半透明版本"
208
+ createFadedMaterialFrom(mesh) {
209
+ const orig = mesh.material;
210
+ if (Array.isArray(orig)) {
211
+ return orig.map(m => this.makeFadedClone(m));
212
+ }
213
+ return this.makeFadedClone(orig);
214
+ }
215
+ /**
216
+ * 设置箭头 Mesh
217
+ */
218
+ setArrowMesh(mesh) {
219
+ this.lxMesh = mesh;
220
+ this.cacheOriginalMaterial(mesh);
221
+ try {
222
+ const mat = mesh.material;
223
+ if (mat && mat.map) {
224
+ const map = mat.map;
225
+ map.wrapS = THREE__namespace.RepeatWrapping;
226
+ map.wrapT = THREE__namespace.RepeatWrapping;
227
+ map.needsUpdate = true;
228
+ }
229
+ mesh.visible = false;
230
+ }
231
+ catch (error) {
232
+ console.error('ArrowGuide: 设置箭头材质失败', error);
233
+ }
234
+ }
235
+ /**
236
+ * 高亮指定模型
237
+ */
238
+ highlight(models) {
239
+ if (!models || models.length === 0) {
240
+ console.warn('ArrowGuide: 高亮模型列表为空');
241
+ return;
242
+ }
243
+ this.modelBrightArr = models;
244
+ this.flowActive = true;
245
+ if (this.lxMesh)
246
+ this.lxMesh.visible = true;
247
+ this.applyHighlight();
248
+ }
249
+ // ✅ 应用高亮效果:非高亮模型保留细节 → 使用"克隆后的半透明材质"
250
+ applyHighlight() {
251
+ // ✨ 使用 Set 提升查找性能
252
+ const keepMeshes = new Set();
253
+ this.modelBrightArr.forEach(obj => {
254
+ obj.traverse(child => {
255
+ if (child.isMesh)
256
+ keepMeshes.add(child);
257
+ });
258
+ });
259
+ try {
260
+ this.scene.traverse(obj => {
261
+ if (obj.isMesh) {
262
+ const mesh = obj;
263
+ // 缓存原材质(用于恢复)
264
+ this.cacheOriginalMaterial(mesh);
265
+ if (!keepMeshes.has(mesh)) {
266
+ // 非高亮:如果还没给它生成过"半透明克隆材质",就创建一次
267
+ if (!this.fadedMaterials.has(mesh)) {
268
+ const faded = this.createFadedMaterialFrom(mesh);
269
+ this.fadedMaterials.set(mesh, faded);
270
+ }
271
+ // 替换为克隆材质(保留所有贴图/法线等细节)
272
+ const fadedMat = this.fadedMaterials.get(mesh);
273
+ if (fadedMat)
274
+ mesh.material = fadedMat;
275
+ }
276
+ else {
277
+ // 高亮对象:确保回到原材质(避免上一次高亮后遗留)
278
+ const orig = this.originalMaterials.get(mesh);
279
+ if (orig && mesh.material !== orig) {
280
+ mesh.material = orig;
281
+ mesh.material.needsUpdate = true;
282
+ }
283
+ }
284
+ }
285
+ });
286
+ }
287
+ catch (error) {
288
+ console.error('ArrowGuide: 应用高亮失败', error);
289
+ }
290
+ }
291
+ // ✅ 恢复为原材质 & 释放克隆材质
292
+ restore() {
293
+ this.flowActive = false;
294
+ if (this.lxMesh)
295
+ this.lxMesh.visible = false;
296
+ try {
297
+ // ✨ 收集所有需要释放的材质
298
+ const materialsToDispose = [];
299
+ this.scene.traverse(obj => {
300
+ if (obj.isMesh) {
301
+ const mesh = obj;
302
+ const orig = this.originalMaterials.get(mesh);
303
+ if (orig) {
304
+ mesh.material = orig;
305
+ mesh.material.needsUpdate = true;
306
+ }
307
+ // ✨ 收集待释放的淡化材质
308
+ const faded = this.fadedMaterials.get(mesh);
309
+ if (faded) {
310
+ if (Array.isArray(faded)) {
311
+ materialsToDispose.push(...faded);
312
+ }
313
+ else {
314
+ materialsToDispose.push(faded);
315
+ }
316
+ }
317
+ }
318
+ });
319
+ // ✨ 批量释放材质(不触碰贴图资源)
320
+ materialsToDispose.forEach(mat => {
321
+ try {
322
+ mat.dispose();
323
+ }
324
+ catch (error) {
325
+ console.error('ArrowGuide: 释放材质失败', error);
326
+ }
327
+ });
328
+ // ✨ 创建新的 WeakMap(相当于清空)
329
+ this.fadedMaterials = new WeakMap();
330
+ }
331
+ catch (error) {
332
+ console.error('ArrowGuide: 恢复材质失败', error);
333
+ }
334
+ }
335
+ /**
336
+ * 动画更新(每帧调用)
337
+ */
338
+ animate() {
339
+ if (!this.flowActive || !this.lxMesh)
340
+ return;
341
+ try {
342
+ const mat = this.lxMesh.material;
343
+ if (mat && mat.map) {
344
+ const map = mat.map;
345
+ map.offset.y -= 0.01;
346
+ map.needsUpdate = true;
347
+ }
348
+ }
349
+ catch (error) {
350
+ console.error('ArrowGuide: 动画更新失败', error);
351
+ }
352
+ }
353
+ /**
354
+ * 初始化事件监听器
355
+ */
356
+ initEvents() {
357
+ const dom = this.renderer.domElement;
358
+ const signal = this.abortController.signal;
359
+ // ✨ 使用 AbortController signal 自动管理事件生命周期
360
+ dom.addEventListener('pointerdown', (e) => {
361
+ this.pointerDownPos.set(e.clientX, e.clientY);
362
+ }, { signal });
363
+ dom.addEventListener('pointerup', (e) => {
364
+ const dx = Math.abs(e.clientX - this.pointerDownPos.x);
365
+ const dy = Math.abs(e.clientY - this.pointerDownPos.y);
366
+ if (dx > this.clickThreshold || dy > this.clickThreshold)
367
+ return; // 拖拽
368
+ const rect = dom.getBoundingClientRect();
369
+ this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
370
+ this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
371
+ this.raycaster.setFromCamera(this.mouse, this.camera);
372
+ const intersects = this.raycaster.intersectObjects(this.scene.children, true);
373
+ const filtered = intersects.filter(i => {
374
+ if (!i.object)
375
+ return false;
376
+ if (this.ignoreRaycastNames.has(i.object.name))
377
+ return false;
378
+ return true;
379
+ });
380
+ if (filtered.length === 0)
381
+ this.restore(); // 点击空白恢复
382
+ }, { signal });
383
+ }
384
+ /**
385
+ * 释放所有资源
386
+ */
387
+ dispose() {
388
+ // ✨ 先恢复材质
389
+ this.restore();
390
+ // ✨ 使用 AbortController 一次性解绑所有事件
391
+ if (this.abortController) {
392
+ this.abortController.abort();
393
+ this.abortController = null;
394
+ }
395
+ // ✨ 清空引用
396
+ this.modelBrightArr = [];
397
+ this.lxMesh = null;
398
+ this.fadedMaterials = new WeakMap();
399
+ this.originalMaterials = new WeakMap();
400
+ this.ignoreRaycastNames.clear();
401
+ }
402
+ }
403
+
404
+ // utils/LiquidFillerGroup.ts
405
+ /**
406
+ * LiquidFillerGroup - 优化版
407
+ * 支持单模型或多模型液位动画、独立颜色控制
408
+ *
409
+ * ✨ 优化内容:
410
+ * - 使用 renderer.domElement 替代 window 事件
411
+ * - 使用 AbortController 管理事件生命周期
412
+ * - 添加错误处理和边界检查
413
+ * - 优化动画管理,避免内存泄漏
414
+ * - 完善资源释放逻辑
415
+ */
416
+ class LiquidFillerGroup {
417
+ /**
418
+ * 构造函数
419
+ * @param models 单个或多个 THREE.Object3D
420
+ * @param scene 场景
421
+ * @param camera 相机
422
+ * @param renderer 渲染器
423
+ * @param defaultOptions 默认液体选项
424
+ * @param clickThreshold 点击阈值,单位像素
425
+ */
426
+ constructor(models, scene, camera, renderer, defaultOptions, clickThreshold = 10) {
427
+ this.items = [];
428
+ this.raycaster = new THREE__namespace.Raycaster();
429
+ this.pointerDownPos = new THREE__namespace.Vector2();
430
+ this.clickThreshold = 10;
431
+ this.abortController = null; // ✨ 事件管理器
432
+ /** pointerdown 记录位置 */
433
+ this.handlePointerDown = (event) => {
434
+ this.pointerDownPos.set(event.clientX, event.clientY);
435
+ };
436
+ /** pointerup 判断点击空白,恢复原始材质 */
437
+ this.handlePointerUp = (event) => {
438
+ const dx = event.clientX - this.pointerDownPos.x;
439
+ const dy = event.clientY - this.pointerDownPos.y;
440
+ const distance = Math.sqrt(dx * dx + dy * dy);
441
+ if (distance > this.clickThreshold)
442
+ return; // 拖拽不触发
443
+ // ✨ 使用 renderer.domElement 的实际尺寸
444
+ const rect = this.renderer.domElement.getBoundingClientRect();
445
+ const pointerNDC = new THREE__namespace.Vector2(((event.clientX - rect.left) / rect.width) * 2 - 1, -((event.clientY - rect.top) / rect.height) * 2 + 1);
446
+ this.raycaster.setFromCamera(pointerNDC, this.camera);
447
+ // 点击空白 -> 所有模型恢复
448
+ const intersectsAny = this.items.some(item => this.raycaster.intersectObject(item.model, true).length > 0);
449
+ if (!intersectsAny) {
450
+ this.restoreAll();
451
+ }
452
+ };
453
+ this.scene = scene;
454
+ this.camera = camera;
455
+ this.renderer = renderer;
456
+ this.clickThreshold = clickThreshold;
457
+ // ✨ 创建 AbortController 用于事件管理
458
+ this.abortController = new AbortController();
459
+ const modelArray = Array.isArray(models) ? models : [models];
460
+ modelArray.forEach(model => {
461
+ var _a, _b, _c;
462
+ try {
463
+ const options = {
464
+ color: (_a = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.color) !== null && _a !== void 0 ? _a : 0x00ff00,
465
+ opacity: (_b = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.opacity) !== null && _b !== void 0 ? _b : 0.6,
466
+ speed: (_c = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.speed) !== null && _c !== void 0 ? _c : 0.05,
467
+ };
468
+ // 保存原始材质
469
+ const originalMaterials = new Map();
470
+ model.traverse(obj => {
471
+ if (obj.isMesh) {
472
+ const mesh = obj;
473
+ originalMaterials.set(mesh, mesh.material);
474
+ }
475
+ });
476
+ // ✨ 边界检查:确保有材质可以保存
477
+ if (originalMaterials.size === 0) {
478
+ console.warn('LiquidFillerGroup: 模型没有 Mesh 对象', model);
479
+ return;
480
+ }
481
+ // 应用淡线框材质
482
+ model.traverse(obj => {
483
+ if (obj.isMesh) {
484
+ const mesh = obj;
485
+ mesh.material = new THREE__namespace.MeshBasicMaterial({
486
+ color: 0xffffff,
487
+ wireframe: true,
488
+ transparent: true,
489
+ opacity: 0.2,
490
+ });
491
+ }
492
+ });
493
+ // 创建液体 Mesh
494
+ const geometries = [];
495
+ model.traverse(obj => {
496
+ if (obj.isMesh) {
497
+ const mesh = obj;
498
+ const geom = mesh.geometry.clone();
499
+ geom.applyMatrix4(mesh.matrixWorld);
500
+ geometries.push(geom);
501
+ }
502
+ });
503
+ if (geometries.length === 0) {
504
+ console.warn('LiquidFillerGroup: 模型没有几何体', model);
505
+ return;
506
+ }
507
+ const mergedGeometry = BufferGeometryUtils__namespace.mergeGeometries(geometries, false);
508
+ if (!mergedGeometry) {
509
+ console.error('LiquidFillerGroup: 几何体合并失败', model);
510
+ return;
511
+ }
512
+ const material = new THREE__namespace.MeshPhongMaterial({
513
+ color: options.color,
514
+ transparent: true,
515
+ opacity: options.opacity,
516
+ side: THREE__namespace.DoubleSide,
517
+ });
518
+ const liquidMesh = new THREE__namespace.Mesh(mergedGeometry, material);
519
+ this.scene.add(liquidMesh);
520
+ // 设置 clippingPlane
521
+ const clipPlane = new THREE__namespace.Plane(new THREE__namespace.Vector3(0, -1, 0), 0);
522
+ const mat = liquidMesh.material;
523
+ mat.clippingPlanes = [clipPlane];
524
+ this.renderer.localClippingEnabled = true;
525
+ this.items.push({
526
+ model,
527
+ liquidMesh,
528
+ clipPlane,
529
+ originalMaterials,
530
+ options,
531
+ animationId: null // ✨ 初始化动画 ID
532
+ });
533
+ }
534
+ catch (error) {
535
+ console.error('LiquidFillerGroup: 初始化模型失败', model, error);
536
+ }
537
+ });
538
+ // ✨ 使用 renderer.domElement 替代 window,使用 AbortController signal
539
+ const signal = this.abortController.signal;
540
+ this.renderer.domElement.addEventListener('pointerdown', this.handlePointerDown, { signal });
541
+ this.renderer.domElement.addEventListener('pointerup', this.handlePointerUp, { signal });
542
+ }
543
+ /**
544
+ * 设置液位
545
+ * @param models 单个模型或模型数组
546
+ * @param percent 液位百分比 0~1
547
+ */
548
+ fillTo(models, percent) {
549
+ // ✨ 边界检查
550
+ if (percent < 0 || percent > 1) {
551
+ console.warn('LiquidFillerGroup: percent 必须在 0~1 之间', percent);
552
+ percent = Math.max(0, Math.min(1, percent));
553
+ }
554
+ const modelArray = Array.isArray(models) ? models : [models];
555
+ modelArray.forEach(model => {
556
+ const item = this.items.find(i => i.model === model);
557
+ if (!item) {
558
+ console.warn('LiquidFillerGroup: 未找到模型', model);
559
+ return;
560
+ }
561
+ if (!item.liquidMesh) {
562
+ console.warn('LiquidFillerGroup: liquidMesh 已被释放', model);
563
+ return;
564
+ }
565
+ // ✨ 取消之前的动画
566
+ if (item.animationId !== null) {
567
+ cancelAnimationFrame(item.animationId);
568
+ item.animationId = null;
569
+ }
570
+ try {
571
+ const box = new THREE__namespace.Box3().setFromObject(item.liquidMesh);
572
+ const min = box.min.y;
573
+ const max = box.max.y;
574
+ const targetHeight = min + (max - min) * percent;
575
+ const animate = () => {
576
+ if (!item.liquidMesh) {
577
+ item.animationId = null;
578
+ return;
579
+ }
580
+ const diff = targetHeight - item.clipPlane.constant;
581
+ if (Math.abs(diff) > 0.01) {
582
+ item.clipPlane.constant += diff * item.options.speed;
583
+ item.animationId = requestAnimationFrame(animate);
584
+ }
585
+ else {
586
+ item.clipPlane.constant = targetHeight;
587
+ item.animationId = null;
588
+ }
589
+ };
590
+ animate();
591
+ }
592
+ catch (error) {
593
+ console.error('LiquidFillerGroup: fillTo 执行失败', model, error);
594
+ }
595
+ });
596
+ }
597
+ /** 设置多个模型液位,percentList 与 items 顺序对应 */
598
+ fillToAll(percentList) {
599
+ if (percentList.length !== this.items.length) {
600
+ console.warn(`LiquidFillerGroup: percentList 长度 (${percentList.length}) 与 items 长度 (${this.items.length}) 不匹配`);
601
+ }
602
+ percentList.forEach((p, idx) => {
603
+ if (idx < this.items.length) {
604
+ this.fillTo(this.items[idx].model, p);
605
+ }
606
+ });
607
+ }
608
+ /** 恢复单个模型原始材质并移除液体 */
609
+ restore(model) {
610
+ const item = this.items.find(i => i.model === model);
611
+ if (!item)
612
+ return;
613
+ // ✨ 取消动画
614
+ if (item.animationId !== null) {
615
+ cancelAnimationFrame(item.animationId);
616
+ item.animationId = null;
617
+ }
618
+ // 恢复原始材质
619
+ item.model.traverse(obj => {
620
+ if (obj.isMesh) {
621
+ const mesh = obj;
622
+ const original = item.originalMaterials.get(mesh);
623
+ if (original)
624
+ mesh.material = original;
625
+ }
626
+ });
627
+ // 释放液体 Mesh
628
+ if (item.liquidMesh) {
629
+ this.scene.remove(item.liquidMesh);
630
+ item.liquidMesh.geometry.dispose();
631
+ if (Array.isArray(item.liquidMesh.material)) {
632
+ item.liquidMesh.material.forEach(m => m.dispose());
633
+ }
634
+ else {
635
+ item.liquidMesh.material.dispose();
636
+ }
637
+ item.liquidMesh = null;
638
+ }
639
+ }
640
+ /** 恢复所有模型 */
641
+ restoreAll() {
642
+ this.items.forEach(item => this.restore(item.model));
643
+ }
644
+ /** 销毁方法,释放事件和资源 */
645
+ dispose() {
646
+ // ✨ 先恢复所有模型
647
+ this.restoreAll();
648
+ // ✨ 使用 AbortController 一次性解绑所有事件
649
+ if (this.abortController) {
650
+ this.abortController.abort();
651
+ this.abortController = null;
652
+ }
653
+ // ✨ 清空 items
654
+ this.items.length = 0;
655
+ }
656
+ }
657
+
658
+ exports.ArrowGuide = ArrowGuide;
659
+ exports.LiquidFillerGroup = LiquidFillerGroup;
660
+ exports.createModelClickHandler = createModelClickHandler;
661
+ //# sourceMappingURL=index.js.map