@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
package/dist/core/index.d.ts
CHANGED
|
@@ -2,6 +2,17 @@ import * as THREE from 'three';
|
|
|
2
2
|
import { OutlinePass } from 'three/examples/jsm/postprocessing/OutlinePass';
|
|
3
3
|
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @file labelManager.ts
|
|
7
|
+
* @description
|
|
8
|
+
* Manages HTML labels attached to 3D objects. Efficiently updates label positions based on camera movement.
|
|
9
|
+
*
|
|
10
|
+
* @best-practice
|
|
11
|
+
* - Use `addChildModelLabels` to label parts of a loaded model.
|
|
12
|
+
* - Labels are HTML elements overlaid on the canvas.
|
|
13
|
+
* - Supports performance optimization via caching and visibility culling.
|
|
14
|
+
*/
|
|
15
|
+
|
|
5
16
|
interface LabelOptions {
|
|
6
17
|
fontSize?: string;
|
|
7
18
|
color?: string;
|
|
@@ -18,23 +29,34 @@ interface LabelManager {
|
|
|
18
29
|
isRunning: () => boolean;
|
|
19
30
|
}
|
|
20
31
|
/**
|
|
21
|
-
*
|
|
32
|
+
* Add overhead labels to child models (supports Mesh and Group)
|
|
22
33
|
*
|
|
23
|
-
*
|
|
24
|
-
* -
|
|
25
|
-
* -
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
34
|
+
* Features:
|
|
35
|
+
* - Caches bounding boxes to avoid repetitive calculation every frame
|
|
36
|
+
* - Supports pause/resume
|
|
37
|
+
* - Configurable update interval to reduce CPU usage
|
|
38
|
+
* - Automatically pauses when hidden
|
|
28
39
|
*
|
|
29
|
-
* @param camera THREE.Camera -
|
|
30
|
-
* @param renderer THREE.WebGLRenderer -
|
|
31
|
-
* @param parentModel THREE.Object3D - FBX
|
|
32
|
-
* @param modelLabelsMap Record<string,string> -
|
|
33
|
-
* @param options LabelOptions -
|
|
34
|
-
* @returns LabelManager -
|
|
40
|
+
* @param camera THREE.Camera - Scene camera
|
|
41
|
+
* @param renderer THREE.WebGLRenderer - Renderer, used for screen size
|
|
42
|
+
* @param parentModel THREE.Object3D - FBX root node or Group
|
|
43
|
+
* @param modelLabelsMap Record<string,string> - Map of model name to label text
|
|
44
|
+
* @param options LabelOptions - Optional label style configuration
|
|
45
|
+
* @returns LabelManager - Management interface containing pause/resume/dispose
|
|
35
46
|
*/
|
|
36
47
|
declare function addChildModelLabels(camera: THREE.Camera, renderer: THREE.WebGLRenderer, parentModel: THREE.Object3D, modelLabelsMap: Record<string, string>, options?: LabelOptions): LabelManager;
|
|
37
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @file hoverEffect.ts
|
|
51
|
+
* @description
|
|
52
|
+
* Singleton highlight effect manager. Uses OutlinePass to create a breathing highlight effect on hovered objects.
|
|
53
|
+
*
|
|
54
|
+
* @best-practice
|
|
55
|
+
* - Initialize once in your setup/mounted hook.
|
|
56
|
+
* - Call `updateHighlightNames` to filter which objects are interactive.
|
|
57
|
+
* - Automatically handles mousemove throttling and cleanup on dispose.
|
|
58
|
+
*/
|
|
59
|
+
|
|
38
60
|
type HoverBreathOptions = {
|
|
39
61
|
camera: THREE.Camera;
|
|
40
62
|
scene: THREE.Scene;
|
|
@@ -47,13 +69,13 @@ type HoverBreathOptions = {
|
|
|
47
69
|
throttleDelay?: number;
|
|
48
70
|
};
|
|
49
71
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
72
|
+
* Create a singleton highlighter - Recommended to create once on mount
|
|
73
|
+
* Returns { updateHighlightNames, dispose, getHoveredName } interface
|
|
52
74
|
*
|
|
53
|
-
*
|
|
54
|
-
* -
|
|
55
|
-
* - mousemove
|
|
56
|
-
* -
|
|
75
|
+
* Features:
|
|
76
|
+
* - Automatically pauses animation when no object is hovered
|
|
77
|
+
* - Throttles mousemove events to avoid excessive calculation
|
|
78
|
+
* - Uses passive event listeners to improve scrolling performance
|
|
57
79
|
*/
|
|
58
80
|
declare function enableHoverBreath(opts: HoverBreathOptions): {
|
|
59
81
|
updateHighlightNames: (names: string[] | null) => void;
|
|
@@ -63,7 +85,18 @@ declare function enableHoverBreath(opts: HoverBreathOptions): {
|
|
|
63
85
|
};
|
|
64
86
|
|
|
65
87
|
/**
|
|
66
|
-
*
|
|
88
|
+
* @file postProcessing.ts
|
|
89
|
+
* @description
|
|
90
|
+
* Manages the post-processing chain, specifically for Outline effects and Gamma correction.
|
|
91
|
+
*
|
|
92
|
+
* @best-practice
|
|
93
|
+
* - call `initPostProcessing` after creating your renderer and scene.
|
|
94
|
+
* - Use the returned `composer` in your render loop instead of `renderer.render`.
|
|
95
|
+
* - Handles resizing automatically via the `resize` method.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Post-processing configuration options
|
|
67
100
|
*/
|
|
68
101
|
interface PostProcessingOptions {
|
|
69
102
|
edgeStrength?: number;
|
|
@@ -74,7 +107,7 @@ interface PostProcessingOptions {
|
|
|
74
107
|
resolutionScale?: number;
|
|
75
108
|
}
|
|
76
109
|
/**
|
|
77
|
-
*
|
|
110
|
+
* Post-processing management interface
|
|
78
111
|
*/
|
|
79
112
|
interface PostProcessingManager {
|
|
80
113
|
composer: EffectComposer;
|
|
@@ -83,18 +116,18 @@ interface PostProcessingManager {
|
|
|
83
116
|
dispose: () => void;
|
|
84
117
|
}
|
|
85
118
|
/**
|
|
86
|
-
*
|
|
119
|
+
* Initialize outline-related information (contains OutlinePass)
|
|
87
120
|
*
|
|
88
|
-
*
|
|
89
|
-
* -
|
|
90
|
-
* -
|
|
91
|
-
* -
|
|
121
|
+
* Capabilities:
|
|
122
|
+
* - Supports automatic update on window resize
|
|
123
|
+
* - Configurable resolution scale for performance improvement
|
|
124
|
+
* - Comprehensive resource disposal management
|
|
92
125
|
*
|
|
93
126
|
* @param renderer THREE.WebGLRenderer
|
|
94
127
|
* @param scene THREE.Scene
|
|
95
128
|
* @param camera THREE.Camera
|
|
96
|
-
* @param options PostProcessingOptions -
|
|
97
|
-
* @returns PostProcessingManager -
|
|
129
|
+
* @param options PostProcessingOptions - Optional configuration
|
|
130
|
+
* @returns PostProcessingManager - Management interface containing composer/outlinePass/resize/dispose
|
|
98
131
|
*/
|
|
99
132
|
declare function initPostProcessing(renderer: THREE.WebGLRenderer, scene: THREE.Scene, camera: THREE.Camera, options?: PostProcessingOptions): PostProcessingManager;
|
|
100
133
|
|
package/dist/core/index.js
CHANGED
|
@@ -27,25 +27,35 @@ function _interopNamespaceDefault(e) {
|
|
|
27
27
|
var THREE__namespace = /*#__PURE__*/_interopNamespaceDefault(THREE);
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
30
|
+
* @file labelManager.ts
|
|
31
|
+
* @description
|
|
32
|
+
* Manages HTML labels attached to 3D objects. Efficiently updates label positions based on camera movement.
|
|
31
33
|
*
|
|
32
|
-
*
|
|
33
|
-
* -
|
|
34
|
-
* -
|
|
35
|
-
* -
|
|
36
|
-
|
|
34
|
+
* @best-practice
|
|
35
|
+
* - Use `addChildModelLabels` to label parts of a loaded model.
|
|
36
|
+
* - Labels are HTML elements overlaid on the canvas.
|
|
37
|
+
* - Supports performance optimization via caching and visibility culling.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Add overhead labels to child models (supports Mesh and Group)
|
|
41
|
+
*
|
|
42
|
+
* Features:
|
|
43
|
+
* - Caches bounding boxes to avoid repetitive calculation every frame
|
|
44
|
+
* - Supports pause/resume
|
|
45
|
+
* - Configurable update interval to reduce CPU usage
|
|
46
|
+
* - Automatically pauses when hidden
|
|
37
47
|
*
|
|
38
|
-
* @param camera THREE.Camera -
|
|
39
|
-
* @param renderer THREE.WebGLRenderer -
|
|
40
|
-
* @param parentModel THREE.Object3D - FBX
|
|
41
|
-
* @param modelLabelsMap Record<string,string> -
|
|
42
|
-
* @param options LabelOptions -
|
|
43
|
-
* @returns LabelManager -
|
|
48
|
+
* @param camera THREE.Camera - Scene camera
|
|
49
|
+
* @param renderer THREE.WebGLRenderer - Renderer, used for screen size
|
|
50
|
+
* @param parentModel THREE.Object3D - FBX root node or Group
|
|
51
|
+
* @param modelLabelsMap Record<string,string> - Map of model name to label text
|
|
52
|
+
* @param options LabelOptions - Optional label style configuration
|
|
53
|
+
* @returns LabelManager - Management interface containing pause/resume/dispose
|
|
44
54
|
*/
|
|
45
55
|
function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, options) {
|
|
46
|
-
//
|
|
56
|
+
// Defensive check: ensure parentModel is loaded
|
|
47
57
|
if (!parentModel || typeof parentModel.traverse !== 'function') {
|
|
48
|
-
console.error('parentModel
|
|
58
|
+
console.error('parentModel invalid, please ensure the FBX model is loaded');
|
|
49
59
|
return {
|
|
50
60
|
pause: () => { },
|
|
51
61
|
resume: () => { },
|
|
@@ -53,48 +63,48 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
|
|
|
53
63
|
isRunning: () => false
|
|
54
64
|
};
|
|
55
65
|
}
|
|
56
|
-
//
|
|
66
|
+
// Configuration
|
|
57
67
|
const enableCache = (options === null || options === void 0 ? void 0 : options.enableCache) !== false;
|
|
58
68
|
const updateInterval = (options === null || options === void 0 ? void 0 : options.updateInterval) || 0;
|
|
59
|
-
//
|
|
69
|
+
// Create label container, absolute positioning, attached to body
|
|
60
70
|
const container = document.createElement('div');
|
|
61
71
|
container.style.position = 'absolute';
|
|
62
72
|
container.style.top = '0';
|
|
63
73
|
container.style.left = '0';
|
|
64
|
-
container.style.pointerEvents = 'none'; //
|
|
74
|
+
container.style.pointerEvents = 'none'; // Avoid blocking mouse events
|
|
65
75
|
container.style.zIndex = '1000';
|
|
66
76
|
document.body.appendChild(container);
|
|
67
77
|
const labels = [];
|
|
68
|
-
//
|
|
78
|
+
// State management
|
|
69
79
|
let rafId = null;
|
|
70
80
|
let isPaused = false;
|
|
71
81
|
let lastUpdateTime = 0;
|
|
72
|
-
//
|
|
82
|
+
// Traverse all child models
|
|
73
83
|
parentModel.traverse((child) => {
|
|
74
84
|
var _a;
|
|
75
|
-
//
|
|
85
|
+
// Only process Mesh or Group
|
|
76
86
|
if ((child.isMesh || child.type === 'Group')) {
|
|
77
|
-
//
|
|
87
|
+
// Dynamic matching of name to prevent undefined
|
|
78
88
|
const labelText = (_a = Object.entries(modelLabelsMap).find(([key]) => child.name.includes(key))) === null || _a === void 0 ? void 0 : _a[1];
|
|
79
89
|
if (!labelText)
|
|
80
|
-
return; //
|
|
81
|
-
//
|
|
90
|
+
return; // Skip if no matching label
|
|
91
|
+
// Create DOM label
|
|
82
92
|
const el = document.createElement('div');
|
|
83
93
|
el.innerText = labelText;
|
|
84
|
-
//
|
|
94
|
+
// Styles defined in JS, can be overridden via options
|
|
85
95
|
el.style.position = 'absolute';
|
|
86
96
|
el.style.color = (options === null || options === void 0 ? void 0 : options.color) || '#fff';
|
|
87
97
|
el.style.background = (options === null || options === void 0 ? void 0 : options.background) || 'rgba(0,0,0,0.6)';
|
|
88
98
|
el.style.padding = (options === null || options === void 0 ? void 0 : options.padding) || '4px 8px';
|
|
89
99
|
el.style.borderRadius = (options === null || options === void 0 ? void 0 : options.borderRadius) || '4px';
|
|
90
100
|
el.style.fontSize = (options === null || options === void 0 ? void 0 : options.fontSize) || '14px';
|
|
91
|
-
el.style.transform = 'translate(-50%, -100%)'; //
|
|
101
|
+
el.style.transform = 'translate(-50%, -100%)'; // Position label directly above the model
|
|
92
102
|
el.style.whiteSpace = 'nowrap';
|
|
93
103
|
el.style.pointerEvents = 'none';
|
|
94
104
|
el.style.transition = 'opacity 0.2s ease';
|
|
95
|
-
//
|
|
105
|
+
// Append to container
|
|
96
106
|
container.appendChild(el);
|
|
97
|
-
//
|
|
107
|
+
// Initialize cache
|
|
98
108
|
const cachedBox = new THREE__namespace.Box3().setFromObject(child);
|
|
99
109
|
const center = new THREE__namespace.Vector3();
|
|
100
110
|
cachedBox.getCenter(center);
|
|
@@ -109,7 +119,7 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
|
|
|
109
119
|
}
|
|
110
120
|
});
|
|
111
121
|
/**
|
|
112
|
-
*
|
|
122
|
+
* Update cached bounding box (called only when model transforms)
|
|
113
123
|
*/
|
|
114
124
|
const updateCache = (labelData) => {
|
|
115
125
|
labelData.cachedBox.setFromObject(labelData.object);
|
|
@@ -119,18 +129,18 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
|
|
|
119
129
|
labelData.needsUpdate = false;
|
|
120
130
|
};
|
|
121
131
|
/**
|
|
122
|
-
*
|
|
132
|
+
* Get object top world coordinates (using cache)
|
|
123
133
|
*/
|
|
124
134
|
const getObjectTopPosition = (labelData) => {
|
|
125
135
|
if (enableCache) {
|
|
126
|
-
//
|
|
136
|
+
// Check if object has transformed
|
|
127
137
|
if (labelData.needsUpdate || labelData.object.matrixWorldNeedsUpdate) {
|
|
128
138
|
updateCache(labelData);
|
|
129
139
|
}
|
|
130
140
|
return labelData.cachedTopPos;
|
|
131
141
|
}
|
|
132
142
|
else {
|
|
133
|
-
//
|
|
143
|
+
// Do not use cache, recalculate every time
|
|
134
144
|
const box = new THREE__namespace.Box3().setFromObject(labelData.object);
|
|
135
145
|
const center = new THREE__namespace.Vector3();
|
|
136
146
|
box.getCenter(center);
|
|
@@ -138,15 +148,15 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
|
|
|
138
148
|
}
|
|
139
149
|
};
|
|
140
150
|
/**
|
|
141
|
-
*
|
|
151
|
+
* Update label positions function
|
|
142
152
|
*/
|
|
143
153
|
function updateLabels(timestamp = 0) {
|
|
144
|
-
//
|
|
154
|
+
// Check pause state
|
|
145
155
|
if (isPaused) {
|
|
146
156
|
rafId = null;
|
|
147
157
|
return;
|
|
148
158
|
}
|
|
149
|
-
//
|
|
159
|
+
// Check update interval
|
|
150
160
|
if (updateInterval > 0 && timestamp - lastUpdateTime < updateInterval) {
|
|
151
161
|
rafId = requestAnimationFrame(updateLabels);
|
|
152
162
|
return;
|
|
@@ -156,22 +166,22 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
|
|
|
156
166
|
const height = renderer.domElement.clientHeight;
|
|
157
167
|
labels.forEach((labelData) => {
|
|
158
168
|
const { el } = labelData;
|
|
159
|
-
const pos = getObjectTopPosition(labelData); //
|
|
160
|
-
pos.project(camera); //
|
|
161
|
-
const x = (pos.x * 0.5 + 0.5) * width; //
|
|
162
|
-
const y = (-(pos.y * 0.5) + 0.5) * height; //
|
|
163
|
-
//
|
|
169
|
+
const pos = getObjectTopPosition(labelData); // Use cached top position
|
|
170
|
+
pos.project(camera); // Convert to screen coordinates
|
|
171
|
+
const x = (pos.x * 0.5 + 0.5) * width; // Screen X
|
|
172
|
+
const y = (-(pos.y * 0.5) + 0.5) * height; // Screen Y
|
|
173
|
+
// Control label visibility (hidden when behind camera)
|
|
164
174
|
const isVisible = pos.z < 1;
|
|
165
175
|
el.style.opacity = isVisible ? '1' : '0';
|
|
166
176
|
el.style.display = isVisible ? 'block' : 'none';
|
|
167
|
-
el.style.transform = `translate(-50%, -100%) translate(${x}px, ${y}px)`; //
|
|
177
|
+
el.style.transform = `translate(-50%, -100%) translate(${x}px, ${y}px)`; // Screen position
|
|
168
178
|
});
|
|
169
|
-
rafId = requestAnimationFrame(updateLabels); //
|
|
179
|
+
rafId = requestAnimationFrame(updateLabels); // Loop update
|
|
170
180
|
}
|
|
171
|
-
//
|
|
181
|
+
// Start update
|
|
172
182
|
updateLabels();
|
|
173
183
|
/**
|
|
174
|
-
*
|
|
184
|
+
* Pause updates
|
|
175
185
|
*/
|
|
176
186
|
const pause = () => {
|
|
177
187
|
isPaused = true;
|
|
@@ -181,7 +191,7 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
|
|
|
181
191
|
}
|
|
182
192
|
};
|
|
183
193
|
/**
|
|
184
|
-
*
|
|
194
|
+
* Resume updates
|
|
185
195
|
*/
|
|
186
196
|
const resume = () => {
|
|
187
197
|
if (!isPaused)
|
|
@@ -190,11 +200,11 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
|
|
|
190
200
|
updateLabels();
|
|
191
201
|
};
|
|
192
202
|
/**
|
|
193
|
-
*
|
|
203
|
+
* Check if running
|
|
194
204
|
*/
|
|
195
205
|
const isRunning = () => !isPaused;
|
|
196
206
|
/**
|
|
197
|
-
*
|
|
207
|
+
* Cleanup function: Remove all DOM labels, cancel animation, avoid memory leaks
|
|
198
208
|
*/
|
|
199
209
|
const dispose = () => {
|
|
200
210
|
pause();
|
|
@@ -216,36 +226,45 @@ function addChildModelLabels(camera, renderer, parentModel, modelLabelsMap, opti
|
|
|
216
226
|
};
|
|
217
227
|
}
|
|
218
228
|
|
|
219
|
-
// src/utils/hoverBreathEffectByNameSingleton.ts
|
|
220
229
|
/**
|
|
221
|
-
*
|
|
222
|
-
*
|
|
230
|
+
* @file hoverEffect.ts
|
|
231
|
+
* @description
|
|
232
|
+
* Singleton highlight effect manager. Uses OutlinePass to create a breathing highlight effect on hovered objects.
|
|
223
233
|
*
|
|
224
|
-
*
|
|
225
|
-
* -
|
|
226
|
-
* -
|
|
227
|
-
* -
|
|
234
|
+
* @best-practice
|
|
235
|
+
* - Initialize once in your setup/mounted hook.
|
|
236
|
+
* - Call `updateHighlightNames` to filter which objects are interactive.
|
|
237
|
+
* - Automatically handles mousemove throttling and cleanup on dispose.
|
|
238
|
+
*/
|
|
239
|
+
/**
|
|
240
|
+
* Create a singleton highlighter - Recommended to create once on mount
|
|
241
|
+
* Returns { updateHighlightNames, dispose, getHoveredName } interface
|
|
242
|
+
*
|
|
243
|
+
* Features:
|
|
244
|
+
* - Automatically pauses animation when no object is hovered
|
|
245
|
+
* - Throttles mousemove events to avoid excessive calculation
|
|
246
|
+
* - Uses passive event listeners to improve scrolling performance
|
|
228
247
|
*/
|
|
229
248
|
function enableHoverBreath(opts) {
|
|
230
|
-
const { camera, scene, renderer, outlinePass, highlightNames = null, minStrength = 2, maxStrength = 5, speed = 4, throttleDelay = 16, //
|
|
249
|
+
const { camera, scene, renderer, outlinePass, highlightNames = null, minStrength = 2, maxStrength = 5, speed = 4, throttleDelay = 16, // Default ~60fps
|
|
231
250
|
} = opts;
|
|
232
251
|
const raycaster = new THREE__namespace.Raycaster();
|
|
233
252
|
const mouse = new THREE__namespace.Vector2();
|
|
234
253
|
let hovered = null;
|
|
235
254
|
let time = 0;
|
|
236
255
|
let animationId = null;
|
|
237
|
-
// highlightSet: null
|
|
256
|
+
// highlightSet: null means all; empty Set means none
|
|
238
257
|
let highlightSet = highlightNames === null ? null : new Set(highlightNames);
|
|
239
|
-
//
|
|
258
|
+
// Throttling related
|
|
240
259
|
let lastMoveTime = 0;
|
|
241
260
|
let rafPending = false;
|
|
242
261
|
function setHighlightNames(names) {
|
|
243
262
|
highlightSet = names === null ? null : new Set(names);
|
|
244
|
-
//
|
|
263
|
+
// If current hovered object is not in the new list, clean up selection immediately
|
|
245
264
|
if (hovered && highlightSet && !highlightSet.has(hovered.name)) {
|
|
246
265
|
hovered = null;
|
|
247
266
|
outlinePass.selectedObjects = [];
|
|
248
|
-
//
|
|
267
|
+
// Pause animation
|
|
249
268
|
if (animationId !== null) {
|
|
250
269
|
cancelAnimationFrame(animationId);
|
|
251
270
|
animationId = null;
|
|
@@ -253,13 +272,13 @@ function enableHoverBreath(opts) {
|
|
|
253
272
|
}
|
|
254
273
|
}
|
|
255
274
|
/**
|
|
256
|
-
*
|
|
275
|
+
* Throttled mousemove handler
|
|
257
276
|
*/
|
|
258
277
|
function onMouseMove(ev) {
|
|
259
278
|
const now = performance.now();
|
|
260
|
-
//
|
|
279
|
+
// Throttle: if time since last process is less than threshold, skip
|
|
261
280
|
if (now - lastMoveTime < throttleDelay) {
|
|
262
|
-
//
|
|
281
|
+
// Use RAF to process the latest event later, ensuring the last event isn't lost
|
|
263
282
|
if (!rafPending) {
|
|
264
283
|
rafPending = true;
|
|
265
284
|
requestAnimationFrame(() => {
|
|
@@ -273,24 +292,24 @@ function enableHoverBreath(opts) {
|
|
|
273
292
|
processMouseMove(ev);
|
|
274
293
|
}
|
|
275
294
|
/**
|
|
276
|
-
*
|
|
295
|
+
* Actual mousemove logic
|
|
277
296
|
*/
|
|
278
297
|
function processMouseMove(ev) {
|
|
279
298
|
const rect = renderer.domElement.getBoundingClientRect();
|
|
280
299
|
mouse.x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
|
|
281
300
|
mouse.y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
|
|
282
301
|
raycaster.setFromCamera(mouse, camera);
|
|
283
|
-
//
|
|
302
|
+
// Deep detect all children of the scene (true)
|
|
284
303
|
const intersects = raycaster.intersectObjects(scene.children, true);
|
|
285
304
|
if (intersects.length > 0) {
|
|
286
305
|
const obj = intersects[0].object;
|
|
287
|
-
//
|
|
306
|
+
// Determine if it is allowed to be highlighted
|
|
288
307
|
const allowed = highlightSet === null ? true : highlightSet.has(obj.name);
|
|
289
308
|
if (allowed) {
|
|
290
309
|
if (hovered !== obj) {
|
|
291
310
|
hovered = obj;
|
|
292
311
|
outlinePass.selectedObjects = [obj];
|
|
293
|
-
//
|
|
312
|
+
// Start animation (if not running)
|
|
294
313
|
if (animationId === null) {
|
|
295
314
|
animate();
|
|
296
315
|
}
|
|
@@ -300,7 +319,7 @@ function enableHoverBreath(opts) {
|
|
|
300
319
|
if (hovered !== null) {
|
|
301
320
|
hovered = null;
|
|
302
321
|
outlinePass.selectedObjects = [];
|
|
303
|
-
//
|
|
322
|
+
// Stop animation
|
|
304
323
|
if (animationId !== null) {
|
|
305
324
|
cancelAnimationFrame(animationId);
|
|
306
325
|
animationId = null;
|
|
@@ -312,7 +331,7 @@ function enableHoverBreath(opts) {
|
|
|
312
331
|
if (hovered !== null) {
|
|
313
332
|
hovered = null;
|
|
314
333
|
outlinePass.selectedObjects = [];
|
|
315
|
-
//
|
|
334
|
+
// Stop animation
|
|
316
335
|
if (animationId !== null) {
|
|
317
336
|
cancelAnimationFrame(animationId);
|
|
318
337
|
animationId = null;
|
|
@@ -321,10 +340,10 @@ function enableHoverBreath(opts) {
|
|
|
321
340
|
}
|
|
322
341
|
}
|
|
323
342
|
/**
|
|
324
|
-
*
|
|
343
|
+
* Animation loop - only runs when there is a hovered object
|
|
325
344
|
*/
|
|
326
345
|
function animate() {
|
|
327
|
-
//
|
|
346
|
+
// If no hovered object, stop animation
|
|
328
347
|
if (!hovered) {
|
|
329
348
|
animationId = null;
|
|
330
349
|
return;
|
|
@@ -334,11 +353,11 @@ function enableHoverBreath(opts) {
|
|
|
334
353
|
const strength = minStrength + ((Math.sin(time) + 1) / 2) * (maxStrength - minStrength);
|
|
335
354
|
outlinePass.edgeStrength = strength;
|
|
336
355
|
}
|
|
337
|
-
//
|
|
338
|
-
//
|
|
356
|
+
// Start (called only once)
|
|
357
|
+
// Use passive to improve scrolling performance
|
|
339
358
|
renderer.domElement.addEventListener('mousemove', onMouseMove, { passive: true });
|
|
340
|
-
//
|
|
341
|
-
// refresh:
|
|
359
|
+
// Note: Do not start animate here, wait until there is a hover object
|
|
360
|
+
// refresh: Forcibly clean up selectedObjects if needed
|
|
342
361
|
function refreshSelection() {
|
|
343
362
|
if (hovered && highlightSet && !highlightSet.has(hovered.name)) {
|
|
344
363
|
hovered = null;
|
|
@@ -359,7 +378,7 @@ function enableHoverBreath(opts) {
|
|
|
359
378
|
animationId = null;
|
|
360
379
|
}
|
|
361
380
|
outlinePass.selectedObjects = [];
|
|
362
|
-
//
|
|
381
|
+
// Clear references
|
|
363
382
|
hovered = null;
|
|
364
383
|
highlightSet = null;
|
|
365
384
|
}
|
|
@@ -372,23 +391,33 @@ function enableHoverBreath(opts) {
|
|
|
372
391
|
}
|
|
373
392
|
|
|
374
393
|
/**
|
|
375
|
-
*
|
|
394
|
+
* @file postProcessing.ts
|
|
395
|
+
* @description
|
|
396
|
+
* Manages the post-processing chain, specifically for Outline effects and Gamma correction.
|
|
397
|
+
*
|
|
398
|
+
* @best-practice
|
|
399
|
+
* - call `initPostProcessing` after creating your renderer and scene.
|
|
400
|
+
* - Use the returned `composer` in your render loop instead of `renderer.render`.
|
|
401
|
+
* - Handles resizing automatically via the `resize` method.
|
|
402
|
+
*/
|
|
403
|
+
/**
|
|
404
|
+
* Initialize outline-related information (contains OutlinePass)
|
|
376
405
|
*
|
|
377
|
-
*
|
|
378
|
-
* -
|
|
379
|
-
* -
|
|
380
|
-
* -
|
|
406
|
+
* Capabilities:
|
|
407
|
+
* - Supports automatic update on window resize
|
|
408
|
+
* - Configurable resolution scale for performance improvement
|
|
409
|
+
* - Comprehensive resource disposal management
|
|
381
410
|
*
|
|
382
411
|
* @param renderer THREE.WebGLRenderer
|
|
383
412
|
* @param scene THREE.Scene
|
|
384
413
|
* @param camera THREE.Camera
|
|
385
|
-
* @param options PostProcessingOptions -
|
|
386
|
-
* @returns PostProcessingManager -
|
|
414
|
+
* @param options PostProcessingOptions - Optional configuration
|
|
415
|
+
* @returns PostProcessingManager - Management interface containing composer/outlinePass/resize/dispose
|
|
387
416
|
*/
|
|
388
417
|
function initPostProcessing(renderer, scene, camera, options = {}) {
|
|
389
|
-
//
|
|
418
|
+
// Default configuration
|
|
390
419
|
const { edgeStrength = 4, edgeGlow = 1, edgeThickness = 2, visibleEdgeColor = '#ffee00', hiddenEdgeColor = '#000000', resolutionScale = 1.0 } = options;
|
|
391
|
-
//
|
|
420
|
+
// Get renderer actual size
|
|
392
421
|
const getSize = () => {
|
|
393
422
|
const width = renderer.domElement.clientWidth;
|
|
394
423
|
const height = renderer.domElement.clientHeight;
|
|
@@ -398,12 +427,12 @@ function initPostProcessing(renderer, scene, camera, options = {}) {
|
|
|
398
427
|
};
|
|
399
428
|
};
|
|
400
429
|
const size = getSize();
|
|
401
|
-
//
|
|
430
|
+
// Create EffectComposer
|
|
402
431
|
const composer = new EffectComposer.EffectComposer(renderer);
|
|
403
|
-
//
|
|
432
|
+
// Basic RenderPass
|
|
404
433
|
const renderPass = new RenderPass.RenderPass(scene, camera);
|
|
405
434
|
composer.addPass(renderPass);
|
|
406
|
-
// OutlinePass
|
|
435
|
+
// OutlinePass for model outlining
|
|
407
436
|
const outlinePass = new OutlinePass.OutlinePass(new THREE__namespace.Vector2(size.width, size.height), scene, camera);
|
|
408
437
|
outlinePass.edgeStrength = edgeStrength;
|
|
409
438
|
outlinePass.edgeGlow = edgeGlow;
|
|
@@ -411,34 +440,34 @@ function initPostProcessing(renderer, scene, camera, options = {}) {
|
|
|
411
440
|
outlinePass.visibleEdgeColor.set(visibleEdgeColor);
|
|
412
441
|
outlinePass.hiddenEdgeColor.set(hiddenEdgeColor);
|
|
413
442
|
composer.addPass(outlinePass);
|
|
414
|
-
// Gamma
|
|
443
|
+
// Gamma correction
|
|
415
444
|
const gammaPass = new ShaderPass.ShaderPass(GammaCorrectionShader.GammaCorrectionShader);
|
|
416
445
|
composer.addPass(gammaPass);
|
|
417
446
|
/**
|
|
418
|
-
* resize
|
|
419
|
-
* @param width
|
|
420
|
-
* @param height
|
|
447
|
+
* Handle resize
|
|
448
|
+
* @param width Optional width, uses renderer.domElement actual width if not provided
|
|
449
|
+
* @param height Optional height, uses renderer.domElement actual height if not provided
|
|
421
450
|
*/
|
|
422
451
|
const resize = (width, height) => {
|
|
423
452
|
const actualSize = width !== undefined && height !== undefined
|
|
424
453
|
? { width: Math.floor(width * resolutionScale), height: Math.floor(height * resolutionScale) }
|
|
425
454
|
: getSize();
|
|
426
|
-
//
|
|
455
|
+
// Update composer size
|
|
427
456
|
composer.setSize(actualSize.width, actualSize.height);
|
|
428
|
-
//
|
|
457
|
+
// Update outlinePass resolution
|
|
429
458
|
outlinePass.resolution.set(actualSize.width, actualSize.height);
|
|
430
459
|
};
|
|
431
460
|
/**
|
|
432
|
-
*
|
|
461
|
+
* Dispose resources
|
|
433
462
|
*/
|
|
434
463
|
const dispose = () => {
|
|
435
|
-
//
|
|
464
|
+
// Dipose all passes
|
|
436
465
|
composer.passes.forEach(pass => {
|
|
437
466
|
if (pass.dispose) {
|
|
438
467
|
pass.dispose();
|
|
439
468
|
}
|
|
440
469
|
});
|
|
441
|
-
//
|
|
470
|
+
// Clear passes array
|
|
442
471
|
composer.passes.length = 0;
|
|
443
472
|
};
|
|
444
473
|
return {
|