@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.
- package/README.md +93 -96
- package/dist/camera/index.d.ts +59 -36
- package/dist/camera/index.js +77 -57
- package/dist/camera/index.js.map +1 -1
- package/dist/camera/index.mjs +77 -57
- package/dist/camera/index.mjs.map +1 -1
- package/dist/core/index.d.ts +60 -27
- package/dist/core/index.js +124 -95
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +124 -95
- package/dist/core/index.mjs.map +1 -1
- package/dist/effect/index.d.ts +47 -134
- package/dist/effect/index.js +109 -65
- package/dist/effect/index.js.map +1 -1
- package/dist/effect/index.mjs +109 -65
- package/dist/effect/index.mjs.map +1 -1
- package/dist/index.d.ts +397 -341
- package/dist/index.js +651 -472
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +651 -472
- package/dist/index.mjs.map +1 -1
- package/dist/interaction/index.d.ts +85 -52
- package/dist/interaction/index.js +161 -133
- package/dist/interaction/index.js.map +1 -1
- package/dist/interaction/index.mjs +161 -133
- package/dist/interaction/index.mjs.map +1 -1
- package/dist/loader/index.d.ts +89 -56
- package/dist/loader/index.js +115 -76
- package/dist/loader/index.js.map +1 -1
- package/dist/loader/index.mjs +115 -76
- package/dist/loader/index.mjs.map +1 -1
- package/dist/setup/index.d.ts +28 -18
- package/dist/setup/index.js +33 -24
- package/dist/setup/index.js.map +1 -1
- package/dist/setup/index.mjs +33 -24
- package/dist/setup/index.mjs.map +1 -1
- package/dist/ui/index.d.ts +18 -7
- package/dist/ui/index.js +32 -22
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/index.mjs +32 -22
- package/dist/ui/index.mjs.map +1 -1
- package/package.json +2 -2
|
@@ -24,28 +24,38 @@ var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
|
|
|
24
24
|
var BufferGeometryUtils__namespace = /*#__PURE__*/_interopNamespaceDefault(BufferGeometryUtils);
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* @file clickHandler.ts
|
|
28
|
+
* @description
|
|
29
|
+
* Tool for handling model clicks and highlighting (OutlinePass version).
|
|
28
30
|
*
|
|
29
|
-
*
|
|
30
|
-
* -
|
|
31
|
-
* -
|
|
32
|
-
* -
|
|
33
|
-
|
|
31
|
+
* @best-practice
|
|
32
|
+
* - Use `createModelClickHandler` to setup interaction.
|
|
33
|
+
* - Handles debouncing and click threshold automatically.
|
|
34
|
+
* - Cleanup using the returned dispose function.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Create Model Click Highlight Tool (OutlinePass Version) - Optimized
|
|
38
|
+
*
|
|
39
|
+
* Features:
|
|
40
|
+
* - Uses AbortController to unify event lifecycle management
|
|
41
|
+
* - Supports debounce to avoid frequent triggering
|
|
42
|
+
* - Customizable Raycaster parameters
|
|
43
|
+
* - Dynamically adjusts outline thickness based on camera distance
|
|
34
44
|
*
|
|
35
|
-
* @param camera
|
|
36
|
-
* @param scene
|
|
37
|
-
* @param renderer
|
|
38
|
-
* @param outlinePass
|
|
39
|
-
* @param onClick
|
|
40
|
-
* @param options
|
|
41
|
-
* @returns
|
|
45
|
+
* @param camera Camera
|
|
46
|
+
* @param scene Scene
|
|
47
|
+
* @param renderer Renderer
|
|
48
|
+
* @param outlinePass Initialized OutlinePass
|
|
49
|
+
* @param onClick Click callback
|
|
50
|
+
* @param options Optional configuration
|
|
51
|
+
* @returns Dispose function, used to clean up events and resources
|
|
42
52
|
*/
|
|
43
53
|
function createModelClickHandler(camera, scene, renderer, outlinePass, onClick, options = {}) {
|
|
44
|
-
//
|
|
54
|
+
// Configuration
|
|
45
55
|
const { clickThreshold = 3, debounceDelay = 0, raycasterParams = {}, enableDynamicThickness = true, minThickness = 1, maxThickness = 10 } = options;
|
|
46
56
|
const raycaster = new THREE__namespace.Raycaster();
|
|
47
57
|
const mouse = new THREE__namespace.Vector2();
|
|
48
|
-
//
|
|
58
|
+
// Apply Raycaster custom parameters
|
|
49
59
|
if (raycasterParams.near !== undefined)
|
|
50
60
|
raycaster.near = raycasterParams.near;
|
|
51
61
|
if (raycasterParams.far !== undefined)
|
|
@@ -62,25 +72,25 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
|
|
|
62
72
|
let startY = 0;
|
|
63
73
|
let selectedObject = null;
|
|
64
74
|
let debounceTimer = null;
|
|
65
|
-
//
|
|
75
|
+
// Use AbortController to manage events uniformly
|
|
66
76
|
const abortController = new AbortController();
|
|
67
77
|
const signal = abortController.signal;
|
|
68
|
-
/**
|
|
78
|
+
/** Restore object highlight (Clear OutlinePass.selectedObjects) */
|
|
69
79
|
function restoreObject() {
|
|
70
80
|
outlinePass.selectedObjects = [];
|
|
71
81
|
}
|
|
72
|
-
/**
|
|
82
|
+
/** Record mouse down position */
|
|
73
83
|
function handleMouseDown(event) {
|
|
74
84
|
startX = event.clientX;
|
|
75
85
|
startY = event.clientY;
|
|
76
86
|
}
|
|
77
|
-
/**
|
|
87
|
+
/** Mouse up determines click or drag (with debounce) */
|
|
78
88
|
function handleMouseUp(event) {
|
|
79
89
|
const dx = Math.abs(event.clientX - startX);
|
|
80
90
|
const dy = Math.abs(event.clientY - startY);
|
|
81
91
|
if (dx > clickThreshold || dy > clickThreshold)
|
|
82
|
-
return; //
|
|
83
|
-
//
|
|
92
|
+
return; // Drag does not trigger click
|
|
93
|
+
// Debounce processing
|
|
84
94
|
if (debounceDelay > 0) {
|
|
85
95
|
if (debounceTimer !== null) {
|
|
86
96
|
clearTimeout(debounceTimer);
|
|
@@ -94,7 +104,7 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
|
|
|
94
104
|
processClick(event);
|
|
95
105
|
}
|
|
96
106
|
}
|
|
97
|
-
/**
|
|
107
|
+
/** Actual click processing logic */
|
|
98
108
|
function processClick(event) {
|
|
99
109
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
100
110
|
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
|
|
@@ -103,55 +113,64 @@ function createModelClickHandler(camera, scene, renderer, outlinePass, onClick,
|
|
|
103
113
|
const intersects = raycaster.intersectObjects(scene.children, true);
|
|
104
114
|
if (intersects.length > 0) {
|
|
105
115
|
let object = intersects[0].object;
|
|
106
|
-
//
|
|
116
|
+
// Click different model, clear previous highlight first
|
|
107
117
|
if (selectedObject && selectedObject !== object)
|
|
108
118
|
restoreObject();
|
|
109
119
|
selectedObject = object;
|
|
110
|
-
// highlightObject(selectedObject); //
|
|
120
|
+
// highlightObject(selectedObject); // Optional: whether to auto highlight
|
|
111
121
|
onClick(selectedObject, {
|
|
112
|
-
name: selectedObject.name || '
|
|
122
|
+
name: selectedObject.name || 'Unnamed Model',
|
|
113
123
|
position: selectedObject.getWorldPosition(new THREE__namespace.Vector3()),
|
|
114
124
|
uuid: selectedObject.uuid
|
|
115
125
|
});
|
|
116
126
|
}
|
|
117
127
|
else {
|
|
118
|
-
//
|
|
128
|
+
// Click blank -> Clear highlight
|
|
119
129
|
if (selectedObject)
|
|
120
130
|
restoreObject();
|
|
121
131
|
selectedObject = null;
|
|
122
132
|
onClick(null);
|
|
123
133
|
}
|
|
124
134
|
}
|
|
125
|
-
//
|
|
135
|
+
// Register events using signal from AbortController
|
|
126
136
|
renderer.domElement.addEventListener('mousedown', handleMouseDown, { signal });
|
|
127
137
|
renderer.domElement.addEventListener('mouseup', handleMouseUp, { signal });
|
|
128
|
-
/**
|
|
138
|
+
/** Dispose function: Unbind events and clear highlight */
|
|
129
139
|
return () => {
|
|
130
|
-
//
|
|
140
|
+
// Clear debounce timer
|
|
131
141
|
if (debounceTimer !== null) {
|
|
132
142
|
clearTimeout(debounceTimer);
|
|
133
143
|
debounceTimer = null;
|
|
134
144
|
}
|
|
135
|
-
//
|
|
145
|
+
// Unbind all events at once
|
|
136
146
|
abortController.abort();
|
|
137
|
-
//
|
|
147
|
+
// Clear highlight
|
|
138
148
|
restoreObject();
|
|
139
|
-
//
|
|
149
|
+
// Clear reference
|
|
140
150
|
selectedObject = null;
|
|
141
151
|
};
|
|
142
152
|
}
|
|
143
153
|
|
|
144
|
-
// src/utils/ArrowGuide.ts
|
|
145
154
|
/**
|
|
146
|
-
*
|
|
147
|
-
*
|
|
155
|
+
* @file arrowGuide.ts
|
|
156
|
+
* @description
|
|
157
|
+
* Arrow guide effect tool, supports highlighting models and fading other objects.
|
|
158
|
+
*
|
|
159
|
+
* @best-practice
|
|
160
|
+
* - Use `highlight` to focus on specific models.
|
|
161
|
+
* - Automatically manages materials and memory using WeakMap.
|
|
162
|
+
* - Call `dispose` when component unmounts.
|
|
163
|
+
*/
|
|
164
|
+
/**
|
|
165
|
+
* ArrowGuide - Optimized Version
|
|
166
|
+
* Arrow guide effect tool, supports highlighting models and fading other objects.
|
|
148
167
|
*
|
|
149
|
-
*
|
|
150
|
-
* -
|
|
151
|
-
* -
|
|
152
|
-
* -
|
|
153
|
-
* -
|
|
154
|
-
* -
|
|
168
|
+
* Features:
|
|
169
|
+
* - Uses WeakMap for automatic material recycling, preventing memory leaks
|
|
170
|
+
* - Uses AbortController to manage event lifecycle
|
|
171
|
+
* - Adds material reuse mechanism to reuse materials
|
|
172
|
+
* - Improved dispose logic ensuring complete resource release
|
|
173
|
+
* - Adds error handling and boundary checks
|
|
155
174
|
*/
|
|
156
175
|
class ArrowGuide {
|
|
157
176
|
constructor(renderer, camera, scene, options) {
|
|
@@ -166,12 +185,12 @@ class ArrowGuide {
|
|
|
166
185
|
this.clickThreshold = 10;
|
|
167
186
|
this.raycaster = new THREE__namespace.Raycaster();
|
|
168
187
|
this.mouse = new THREE__namespace.Vector2();
|
|
169
|
-
//
|
|
188
|
+
// Use WeakMap for automatic material recycling (GC friendly)
|
|
170
189
|
this.originalMaterials = new WeakMap();
|
|
171
190
|
this.fadedMaterials = new WeakMap();
|
|
172
|
-
//
|
|
191
|
+
// AbortController for event management
|
|
173
192
|
this.abortController = null;
|
|
174
|
-
//
|
|
193
|
+
// Config: Non-highlight opacity and brightness
|
|
175
194
|
this.fadeOpacity = 0.5;
|
|
176
195
|
this.fadeBrightness = 0.1;
|
|
177
196
|
this.clickThreshold = (_a = options === null || options === void 0 ? void 0 : options.clickThreshold) !== null && _a !== void 0 ? _a : 10;
|
|
@@ -181,30 +200,30 @@ class ArrowGuide {
|
|
|
181
200
|
this.abortController = new AbortController();
|
|
182
201
|
this.initEvents();
|
|
183
202
|
}
|
|
184
|
-
//
|
|
203
|
+
// Tool: Cache original material (first time only)
|
|
185
204
|
cacheOriginalMaterial(mesh) {
|
|
186
205
|
if (!this.originalMaterials.has(mesh)) {
|
|
187
206
|
this.originalMaterials.set(mesh, mesh.material);
|
|
188
207
|
}
|
|
189
208
|
}
|
|
190
|
-
//
|
|
209
|
+
// Tool: Clone a "translucent version" for a material, preserving all maps and parameters
|
|
191
210
|
makeFadedClone(mat) {
|
|
192
211
|
const clone = mat.clone();
|
|
193
212
|
const c = clone;
|
|
194
|
-
//
|
|
213
|
+
// Only modify transparency parameters, do not modify detail maps like map / normalMap / roughnessMap
|
|
195
214
|
c.transparent = true;
|
|
196
215
|
if (typeof c.opacity === 'number')
|
|
197
216
|
c.opacity = this.fadeOpacity;
|
|
198
217
|
if (c.color && c.color.isColor) {
|
|
199
|
-
c.color.multiplyScalar(this.fadeBrightness); //
|
|
218
|
+
c.color.multiplyScalar(this.fadeBrightness); // Darken color overall
|
|
200
219
|
}
|
|
201
|
-
//
|
|
220
|
+
// Common strategy for fluid display behind transparent objects: do not write depth, only test depth
|
|
202
221
|
clone.depthWrite = false;
|
|
203
222
|
clone.depthTest = true;
|
|
204
223
|
clone.needsUpdate = true;
|
|
205
224
|
return clone;
|
|
206
225
|
}
|
|
207
|
-
//
|
|
226
|
+
// Tool: Batch clone "translucent version" for mesh.material (could be array)
|
|
208
227
|
createFadedMaterialFrom(mesh) {
|
|
209
228
|
const orig = mesh.material;
|
|
210
229
|
if (Array.isArray(orig)) {
|
|
@@ -213,7 +232,7 @@ class ArrowGuide {
|
|
|
213
232
|
return this.makeFadedClone(orig);
|
|
214
233
|
}
|
|
215
234
|
/**
|
|
216
|
-
*
|
|
235
|
+
* Set Arrow Mesh
|
|
217
236
|
*/
|
|
218
237
|
setArrowMesh(mesh) {
|
|
219
238
|
this.lxMesh = mesh;
|
|
@@ -229,15 +248,15 @@ class ArrowGuide {
|
|
|
229
248
|
mesh.visible = false;
|
|
230
249
|
}
|
|
231
250
|
catch (error) {
|
|
232
|
-
console.error('ArrowGuide:
|
|
251
|
+
console.error('ArrowGuide: Failed to set arrow material', error);
|
|
233
252
|
}
|
|
234
253
|
}
|
|
235
254
|
/**
|
|
236
|
-
*
|
|
255
|
+
* Highlight specified models
|
|
237
256
|
*/
|
|
238
257
|
highlight(models) {
|
|
239
258
|
if (!models || models.length === 0) {
|
|
240
|
-
console.warn('ArrowGuide:
|
|
259
|
+
console.warn('ArrowGuide: Highlight model list is empty');
|
|
241
260
|
return;
|
|
242
261
|
}
|
|
243
262
|
this.modelBrightArr = models;
|
|
@@ -246,9 +265,9 @@ class ArrowGuide {
|
|
|
246
265
|
this.lxMesh.visible = true;
|
|
247
266
|
this.applyHighlight();
|
|
248
267
|
}
|
|
249
|
-
//
|
|
268
|
+
// Apply highlight effect: Non-highlighted models preserve details -> use "cloned translucent material"
|
|
250
269
|
applyHighlight() {
|
|
251
|
-
//
|
|
270
|
+
// Use Set to improve lookup performance
|
|
252
271
|
const keepMeshes = new Set();
|
|
253
272
|
this.modelBrightArr.forEach(obj => {
|
|
254
273
|
obj.traverse(child => {
|
|
@@ -260,21 +279,21 @@ class ArrowGuide {
|
|
|
260
279
|
this.scene.traverse(obj => {
|
|
261
280
|
if (obj.isMesh) {
|
|
262
281
|
const mesh = obj;
|
|
263
|
-
//
|
|
282
|
+
// Cache original material (for restoration)
|
|
264
283
|
this.cacheOriginalMaterial(mesh);
|
|
265
284
|
if (!keepMeshes.has(mesh)) {
|
|
266
|
-
//
|
|
285
|
+
// Non-highlighted: if no "translucent clone material" generated yet, create one
|
|
267
286
|
if (!this.fadedMaterials.has(mesh)) {
|
|
268
287
|
const faded = this.createFadedMaterialFrom(mesh);
|
|
269
288
|
this.fadedMaterials.set(mesh, faded);
|
|
270
289
|
}
|
|
271
|
-
//
|
|
290
|
+
// Replace with clone material (preserve all maps/normals details)
|
|
272
291
|
const fadedMat = this.fadedMaterials.get(mesh);
|
|
273
292
|
if (fadedMat)
|
|
274
293
|
mesh.material = fadedMat;
|
|
275
294
|
}
|
|
276
295
|
else {
|
|
277
|
-
//
|
|
296
|
+
// Highlighted object: ensure return to original material (avoid leftover from previous highlight)
|
|
278
297
|
const orig = this.originalMaterials.get(mesh);
|
|
279
298
|
if (orig && mesh.material !== orig) {
|
|
280
299
|
mesh.material = orig;
|
|
@@ -285,16 +304,16 @@ class ArrowGuide {
|
|
|
285
304
|
});
|
|
286
305
|
}
|
|
287
306
|
catch (error) {
|
|
288
|
-
console.error('ArrowGuide:
|
|
307
|
+
console.error('ArrowGuide: Failed to apply highlight', error);
|
|
289
308
|
}
|
|
290
309
|
}
|
|
291
|
-
//
|
|
310
|
+
// Restore to original material & dispose clone material
|
|
292
311
|
restore() {
|
|
293
312
|
this.flowActive = false;
|
|
294
313
|
if (this.lxMesh)
|
|
295
314
|
this.lxMesh.visible = false;
|
|
296
315
|
try {
|
|
297
|
-
//
|
|
316
|
+
// Collect all materials to dispose
|
|
298
317
|
const materialsToDispose = [];
|
|
299
318
|
this.scene.traverse(obj => {
|
|
300
319
|
if (obj.isMesh) {
|
|
@@ -304,7 +323,7 @@ class ArrowGuide {
|
|
|
304
323
|
mesh.material = orig;
|
|
305
324
|
mesh.material.needsUpdate = true;
|
|
306
325
|
}
|
|
307
|
-
//
|
|
326
|
+
// Collect faded materials to dispose
|
|
308
327
|
const faded = this.fadedMaterials.get(mesh);
|
|
309
328
|
if (faded) {
|
|
310
329
|
if (Array.isArray(faded)) {
|
|
@@ -316,24 +335,24 @@ class ArrowGuide {
|
|
|
316
335
|
}
|
|
317
336
|
}
|
|
318
337
|
});
|
|
319
|
-
//
|
|
338
|
+
// Batch dispose materials (do not touch texture resources)
|
|
320
339
|
materialsToDispose.forEach(mat => {
|
|
321
340
|
try {
|
|
322
341
|
mat.dispose();
|
|
323
342
|
}
|
|
324
343
|
catch (error) {
|
|
325
|
-
console.error('ArrowGuide:
|
|
344
|
+
console.error('ArrowGuide: Failed to dispose material', error);
|
|
326
345
|
}
|
|
327
346
|
});
|
|
328
|
-
//
|
|
347
|
+
// Create new WeakMap (equivalent to clearing)
|
|
329
348
|
this.fadedMaterials = new WeakMap();
|
|
330
349
|
}
|
|
331
350
|
catch (error) {
|
|
332
|
-
console.error('ArrowGuide:
|
|
351
|
+
console.error('ArrowGuide: Failed to restore material', error);
|
|
333
352
|
}
|
|
334
353
|
}
|
|
335
354
|
/**
|
|
336
|
-
*
|
|
355
|
+
* Animation update (called every frame)
|
|
337
356
|
*/
|
|
338
357
|
animate() {
|
|
339
358
|
if (!this.flowActive || !this.lxMesh)
|
|
@@ -347,16 +366,16 @@ class ArrowGuide {
|
|
|
347
366
|
}
|
|
348
367
|
}
|
|
349
368
|
catch (error) {
|
|
350
|
-
console.error('ArrowGuide:
|
|
369
|
+
console.error('ArrowGuide: Animation update failed', error);
|
|
351
370
|
}
|
|
352
371
|
}
|
|
353
372
|
/**
|
|
354
|
-
*
|
|
373
|
+
* Initialize event listeners
|
|
355
374
|
*/
|
|
356
375
|
initEvents() {
|
|
357
376
|
const dom = this.renderer.domElement;
|
|
358
377
|
const signal = this.abortController.signal;
|
|
359
|
-
//
|
|
378
|
+
// Use AbortController signal to automatically manage event lifecycle
|
|
360
379
|
dom.addEventListener('pointerdown', (e) => {
|
|
361
380
|
this.pointerDownPos.set(e.clientX, e.clientY);
|
|
362
381
|
}, { signal });
|
|
@@ -364,7 +383,7 @@ class ArrowGuide {
|
|
|
364
383
|
const dx = Math.abs(e.clientX - this.pointerDownPos.x);
|
|
365
384
|
const dy = Math.abs(e.clientY - this.pointerDownPos.y);
|
|
366
385
|
if (dx > this.clickThreshold || dy > this.clickThreshold)
|
|
367
|
-
return; //
|
|
386
|
+
return; // Dragging
|
|
368
387
|
const rect = dom.getBoundingClientRect();
|
|
369
388
|
this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
|
370
389
|
this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
|
@@ -378,21 +397,21 @@ class ArrowGuide {
|
|
|
378
397
|
return true;
|
|
379
398
|
});
|
|
380
399
|
if (filtered.length === 0)
|
|
381
|
-
this.restore(); //
|
|
400
|
+
this.restore(); // Click blank space to restore
|
|
382
401
|
}, { signal });
|
|
383
402
|
}
|
|
384
403
|
/**
|
|
385
|
-
*
|
|
404
|
+
* Dispose all resources
|
|
386
405
|
*/
|
|
387
406
|
dispose() {
|
|
388
|
-
//
|
|
407
|
+
// Restore materials first
|
|
389
408
|
this.restore();
|
|
390
|
-
//
|
|
409
|
+
// Unbind all events at once using AbortController
|
|
391
410
|
if (this.abortController) {
|
|
392
411
|
this.abortController.abort();
|
|
393
412
|
this.abortController = null;
|
|
394
413
|
}
|
|
395
|
-
//
|
|
414
|
+
// Clear references
|
|
396
415
|
this.modelBrightArr = [];
|
|
397
416
|
this.lxMesh = null;
|
|
398
417
|
this.fadedMaterials = new WeakMap();
|
|
@@ -401,50 +420,59 @@ class ArrowGuide {
|
|
|
401
420
|
}
|
|
402
421
|
}
|
|
403
422
|
|
|
404
|
-
// utils/LiquidFillerGroup.ts
|
|
405
423
|
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
424
|
+
* @file liquidFiller.ts
|
|
425
|
+
* @description
|
|
426
|
+
* Liquid filling effect for single or multiple models using local clipping planes.
|
|
408
427
|
*
|
|
409
|
-
*
|
|
410
|
-
* -
|
|
411
|
-
* -
|
|
412
|
-
* -
|
|
413
|
-
|
|
414
|
-
|
|
428
|
+
* @best-practice
|
|
429
|
+
* - Use `fillTo` to animate liquid level.
|
|
430
|
+
* - Supports multiple independent liquid levels.
|
|
431
|
+
* - Call `dispose` to clean up resources and event listeners.
|
|
432
|
+
*/
|
|
433
|
+
/**
|
|
434
|
+
* LiquidFillerGroup - Optimized
|
|
435
|
+
* Supports single or multi-model liquid level animation with independent color control.
|
|
436
|
+
*
|
|
437
|
+
* Features:
|
|
438
|
+
* - Uses renderer.domElement instead of window events
|
|
439
|
+
* - Uses AbortController to manage event lifecycle
|
|
440
|
+
* - Adds error handling and boundary checks
|
|
441
|
+
* - Optimized animation management to prevent memory leaks
|
|
442
|
+
* - Comprehensive resource disposal logic
|
|
415
443
|
*/
|
|
416
444
|
class LiquidFillerGroup {
|
|
417
445
|
/**
|
|
418
|
-
*
|
|
419
|
-
* @param models
|
|
420
|
-
* @param scene
|
|
421
|
-
* @param camera
|
|
422
|
-
* @param renderer
|
|
423
|
-
* @param defaultOptions
|
|
424
|
-
* @param clickThreshold
|
|
446
|
+
* Constructor
|
|
447
|
+
* @param models Single or multiple THREE.Object3D
|
|
448
|
+
* @param scene Scene
|
|
449
|
+
* @param camera Camera
|
|
450
|
+
* @param renderer Renderer
|
|
451
|
+
* @param defaultOptions Default liquid options
|
|
452
|
+
* @param clickThreshold Click threshold in pixels
|
|
425
453
|
*/
|
|
426
454
|
constructor(models, scene, camera, renderer, defaultOptions, clickThreshold = 10) {
|
|
427
455
|
this.items = [];
|
|
428
456
|
this.raycaster = new THREE__namespace.Raycaster();
|
|
429
457
|
this.pointerDownPos = new THREE__namespace.Vector2();
|
|
430
458
|
this.clickThreshold = 10;
|
|
431
|
-
this.abortController = null; //
|
|
432
|
-
/** pointerdown
|
|
459
|
+
this.abortController = null; // Event manager
|
|
460
|
+
/** pointerdown record position */
|
|
433
461
|
this.handlePointerDown = (event) => {
|
|
434
462
|
this.pointerDownPos.set(event.clientX, event.clientY);
|
|
435
463
|
};
|
|
436
|
-
/** pointerup
|
|
464
|
+
/** pointerup check click blank, restore original material */
|
|
437
465
|
this.handlePointerUp = (event) => {
|
|
438
466
|
const dx = event.clientX - this.pointerDownPos.x;
|
|
439
467
|
const dy = event.clientY - this.pointerDownPos.y;
|
|
440
468
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
441
469
|
if (distance > this.clickThreshold)
|
|
442
|
-
return; //
|
|
443
|
-
//
|
|
470
|
+
return; // Do not trigger on drag
|
|
471
|
+
// Use renderer.domElement actual size
|
|
444
472
|
const rect = this.renderer.domElement.getBoundingClientRect();
|
|
445
473
|
const pointerNDC = new THREE__namespace.Vector2(((event.clientX - rect.left) / rect.width) * 2 - 1, -((event.clientY - rect.top) / rect.height) * 2 + 1);
|
|
446
474
|
this.raycaster.setFromCamera(pointerNDC, this.camera);
|
|
447
|
-
//
|
|
475
|
+
// Click blank -> Restore all models
|
|
448
476
|
const intersectsAny = this.items.some(item => this.raycaster.intersectObject(item.model, true).length > 0);
|
|
449
477
|
if (!intersectsAny) {
|
|
450
478
|
this.restoreAll();
|
|
@@ -454,7 +482,7 @@ class LiquidFillerGroup {
|
|
|
454
482
|
this.camera = camera;
|
|
455
483
|
this.renderer = renderer;
|
|
456
484
|
this.clickThreshold = clickThreshold;
|
|
457
|
-
//
|
|
485
|
+
// Create AbortController for event management
|
|
458
486
|
this.abortController = new AbortController();
|
|
459
487
|
const modelArray = Array.isArray(models) ? models : [models];
|
|
460
488
|
modelArray.forEach(model => {
|
|
@@ -465,7 +493,7 @@ class LiquidFillerGroup {
|
|
|
465
493
|
opacity: (_b = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.opacity) !== null && _b !== void 0 ? _b : 0.6,
|
|
466
494
|
speed: (_c = defaultOptions === null || defaultOptions === void 0 ? void 0 : defaultOptions.speed) !== null && _c !== void 0 ? _c : 0.05,
|
|
467
495
|
};
|
|
468
|
-
//
|
|
496
|
+
// Save original materials
|
|
469
497
|
const originalMaterials = new Map();
|
|
470
498
|
model.traverse(obj => {
|
|
471
499
|
if (obj.isMesh) {
|
|
@@ -473,12 +501,12 @@ class LiquidFillerGroup {
|
|
|
473
501
|
originalMaterials.set(mesh, mesh.material);
|
|
474
502
|
}
|
|
475
503
|
});
|
|
476
|
-
//
|
|
504
|
+
// Boundary check: ensure there are materials to save
|
|
477
505
|
if (originalMaterials.size === 0) {
|
|
478
|
-
console.warn('LiquidFillerGroup:
|
|
506
|
+
console.warn('LiquidFillerGroup: Model has no Mesh objects', model);
|
|
479
507
|
return;
|
|
480
508
|
}
|
|
481
|
-
//
|
|
509
|
+
// Apply faded wireframe material
|
|
482
510
|
model.traverse(obj => {
|
|
483
511
|
if (obj.isMesh) {
|
|
484
512
|
const mesh = obj;
|
|
@@ -490,7 +518,7 @@ class LiquidFillerGroup {
|
|
|
490
518
|
});
|
|
491
519
|
}
|
|
492
520
|
});
|
|
493
|
-
//
|
|
521
|
+
// Create liquid Mesh
|
|
494
522
|
const geometries = [];
|
|
495
523
|
model.traverse(obj => {
|
|
496
524
|
if (obj.isMesh) {
|
|
@@ -501,12 +529,12 @@ class LiquidFillerGroup {
|
|
|
501
529
|
}
|
|
502
530
|
});
|
|
503
531
|
if (geometries.length === 0) {
|
|
504
|
-
console.warn('LiquidFillerGroup:
|
|
532
|
+
console.warn('LiquidFillerGroup: Model has no geometries', model);
|
|
505
533
|
return;
|
|
506
534
|
}
|
|
507
535
|
const mergedGeometry = BufferGeometryUtils__namespace.mergeGeometries(geometries, false);
|
|
508
536
|
if (!mergedGeometry) {
|
|
509
|
-
console.error('LiquidFillerGroup:
|
|
537
|
+
console.error('LiquidFillerGroup: Failed to merge geometries', model);
|
|
510
538
|
return;
|
|
511
539
|
}
|
|
512
540
|
const material = new THREE__namespace.MeshPhongMaterial({
|
|
@@ -517,7 +545,7 @@ class LiquidFillerGroup {
|
|
|
517
545
|
});
|
|
518
546
|
const liquidMesh = new THREE__namespace.Mesh(mergedGeometry, material);
|
|
519
547
|
this.scene.add(liquidMesh);
|
|
520
|
-
//
|
|
548
|
+
// Set clippingPlane
|
|
521
549
|
const clipPlane = new THREE__namespace.Plane(new THREE__namespace.Vector3(0, -1, 0), 0);
|
|
522
550
|
const mat = liquidMesh.material;
|
|
523
551
|
mat.clippingPlanes = [clipPlane];
|
|
@@ -528,41 +556,41 @@ class LiquidFillerGroup {
|
|
|
528
556
|
clipPlane,
|
|
529
557
|
originalMaterials,
|
|
530
558
|
options,
|
|
531
|
-
animationId: null //
|
|
559
|
+
animationId: null // Initialize animation ID
|
|
532
560
|
});
|
|
533
561
|
}
|
|
534
562
|
catch (error) {
|
|
535
|
-
console.error('LiquidFillerGroup:
|
|
563
|
+
console.error('LiquidFillerGroup: Failed to initialize model', model, error);
|
|
536
564
|
}
|
|
537
565
|
});
|
|
538
|
-
//
|
|
566
|
+
// Use renderer.domElement instead of window, use AbortController signal
|
|
539
567
|
const signal = this.abortController.signal;
|
|
540
568
|
this.renderer.domElement.addEventListener('pointerdown', this.handlePointerDown, { signal });
|
|
541
569
|
this.renderer.domElement.addEventListener('pointerup', this.handlePointerUp, { signal });
|
|
542
570
|
}
|
|
543
571
|
/**
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
572
|
+
* Set liquid level
|
|
573
|
+
* @param models Single model or array of models
|
|
574
|
+
* @param percent Liquid level percentage 0~1
|
|
575
|
+
*/
|
|
548
576
|
fillTo(models, percent) {
|
|
549
|
-
//
|
|
577
|
+
// Boundary check
|
|
550
578
|
if (percent < 0 || percent > 1) {
|
|
551
|
-
console.warn('LiquidFillerGroup: percent
|
|
579
|
+
console.warn('LiquidFillerGroup: percent must be between 0 and 1', percent);
|
|
552
580
|
percent = Math.max(0, Math.min(1, percent));
|
|
553
581
|
}
|
|
554
582
|
const modelArray = Array.isArray(models) ? models : [models];
|
|
555
583
|
modelArray.forEach(model => {
|
|
556
584
|
const item = this.items.find(i => i.model === model);
|
|
557
585
|
if (!item) {
|
|
558
|
-
console.warn('LiquidFillerGroup:
|
|
586
|
+
console.warn('LiquidFillerGroup: Model not found', model);
|
|
559
587
|
return;
|
|
560
588
|
}
|
|
561
589
|
if (!item.liquidMesh) {
|
|
562
|
-
console.warn('LiquidFillerGroup: liquidMesh
|
|
590
|
+
console.warn('LiquidFillerGroup: liquidMesh already disposed', model);
|
|
563
591
|
return;
|
|
564
592
|
}
|
|
565
|
-
//
|
|
593
|
+
// Cancel previous animation
|
|
566
594
|
if (item.animationId !== null) {
|
|
567
595
|
cancelAnimationFrame(item.animationId);
|
|
568
596
|
item.animationId = null;
|
|
@@ -590,14 +618,14 @@ class LiquidFillerGroup {
|
|
|
590
618
|
animate();
|
|
591
619
|
}
|
|
592
620
|
catch (error) {
|
|
593
|
-
console.error('LiquidFillerGroup: fillTo
|
|
621
|
+
console.error('LiquidFillerGroup: fillTo execution failed', model, error);
|
|
594
622
|
}
|
|
595
623
|
});
|
|
596
624
|
}
|
|
597
|
-
/**
|
|
625
|
+
/** Set multiple model levels, percentList corresponds to items order */
|
|
598
626
|
fillToAll(percentList) {
|
|
599
627
|
if (percentList.length !== this.items.length) {
|
|
600
|
-
console.warn(`LiquidFillerGroup: percentList
|
|
628
|
+
console.warn(`LiquidFillerGroup: percentList length (${percentList.length}) does not match items length (${this.items.length})`);
|
|
601
629
|
}
|
|
602
630
|
percentList.forEach((p, idx) => {
|
|
603
631
|
if (idx < this.items.length) {
|
|
@@ -605,17 +633,17 @@ class LiquidFillerGroup {
|
|
|
605
633
|
}
|
|
606
634
|
});
|
|
607
635
|
}
|
|
608
|
-
/**
|
|
636
|
+
/** Restore single model original material and remove liquid */
|
|
609
637
|
restore(model) {
|
|
610
638
|
const item = this.items.find(i => i.model === model);
|
|
611
639
|
if (!item)
|
|
612
640
|
return;
|
|
613
|
-
//
|
|
641
|
+
// Cancel animation
|
|
614
642
|
if (item.animationId !== null) {
|
|
615
643
|
cancelAnimationFrame(item.animationId);
|
|
616
644
|
item.animationId = null;
|
|
617
645
|
}
|
|
618
|
-
//
|
|
646
|
+
// Restore original material
|
|
619
647
|
item.model.traverse(obj => {
|
|
620
648
|
if (obj.isMesh) {
|
|
621
649
|
const mesh = obj;
|
|
@@ -624,7 +652,7 @@ class LiquidFillerGroup {
|
|
|
624
652
|
mesh.material = original;
|
|
625
653
|
}
|
|
626
654
|
});
|
|
627
|
-
//
|
|
655
|
+
// Dispose liquid Mesh
|
|
628
656
|
if (item.liquidMesh) {
|
|
629
657
|
this.scene.remove(item.liquidMesh);
|
|
630
658
|
item.liquidMesh.geometry.dispose();
|
|
@@ -637,20 +665,20 @@ class LiquidFillerGroup {
|
|
|
637
665
|
item.liquidMesh = null;
|
|
638
666
|
}
|
|
639
667
|
}
|
|
640
|
-
/**
|
|
668
|
+
/** Restore all models */
|
|
641
669
|
restoreAll() {
|
|
642
670
|
this.items.forEach(item => this.restore(item.model));
|
|
643
671
|
}
|
|
644
|
-
/**
|
|
672
|
+
/** Dispose method, release events and resources */
|
|
645
673
|
dispose() {
|
|
646
|
-
//
|
|
674
|
+
// Restore all models first
|
|
647
675
|
this.restoreAll();
|
|
648
|
-
//
|
|
676
|
+
// Unbind all events at once using AbortController
|
|
649
677
|
if (this.abortController) {
|
|
650
678
|
this.abortController.abort();
|
|
651
679
|
this.abortController = null;
|
|
652
680
|
}
|
|
653
|
-
//
|
|
681
|
+
// Clear items
|
|
654
682
|
this.items.length = 0;
|
|
655
683
|
}
|
|
656
684
|
}
|