easy-threesdk 1.0.0
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 +166 -0
- package/index.js +898 -0
- package/package.json +15 -0
- package/plugins/LabelControl.js +202 -0
- package/plugins/ModelControl.js +281 -0
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "easy-threesdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "����Threejs��װ�ij��ù��ܵ�sdk",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"private": false,
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"Threejs"
|
|
12
|
+
],
|
|
13
|
+
"author": "coderxq",
|
|
14
|
+
"license": "ISC"
|
|
15
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|
3
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
4
|
+
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
|
|
5
|
+
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
|
|
6
|
+
import {
|
|
7
|
+
CSS3DRenderer,
|
|
8
|
+
CSS3DObject,
|
|
9
|
+
} from "three/examples/jsm/renderers/CSS3DRenderer.js";
|
|
10
|
+
import { ThreeMFLoader } from "three/examples/jsm/Addons.js";
|
|
11
|
+
|
|
12
|
+
export default class LabelControl {
|
|
13
|
+
_LabelMap = new Map();
|
|
14
|
+
constructor(container, ThreeSdk) {
|
|
15
|
+
this.container = container;
|
|
16
|
+
this.ThreeSdk = ThreeSdk;
|
|
17
|
+
this._init();
|
|
18
|
+
}
|
|
19
|
+
_init() {
|
|
20
|
+
// 创建 CSS3D 渲染器用于渲染 HTML 标签
|
|
21
|
+
this.labelRenderer = new CSS3DRenderer();
|
|
22
|
+
this.labelRenderer.setSize(
|
|
23
|
+
this.container.clientWidth,
|
|
24
|
+
this.container.clientHeight
|
|
25
|
+
);
|
|
26
|
+
this.labelRenderer.domElement.style.position = "absolute";
|
|
27
|
+
this.labelRenderer.domElement.style.top = "0";
|
|
28
|
+
this.labelRenderer.domElement.style.left = "0";
|
|
29
|
+
this.labelRenderer.domElement.style.pointerEvents = "none"; // 容器默认不拦截鼠标事件,但标签本身会处理
|
|
30
|
+
this.container.appendChild(this.labelRenderer.domElement);
|
|
31
|
+
|
|
32
|
+
window.addEventListener("resize", () => {
|
|
33
|
+
this.labelRenderer.setSize(
|
|
34
|
+
this.container.clientWidth,
|
|
35
|
+
this.container.clientHeight
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
this._animate();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
_animate() {
|
|
42
|
+
this.labelRenderer.render(this.ThreeSdk.scene, this.ThreeSdk.camera);
|
|
43
|
+
requestAnimationFrame(this._animate.bind(this));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 创建3D标签并放在指定的模型上面进行标注
|
|
48
|
+
* @param {string} LabelID - 标签ID
|
|
49
|
+
* @param {string} contentHtml - 标签显示的HTML内容
|
|
50
|
+
* @param {THREE.Vector3|Array} position - 标签的3D位置,可以是Vector3对象或[x, y, z]数组
|
|
51
|
+
* @param {Object} options - 可选配置项
|
|
52
|
+
* @param {number} options.size - 标签的缩放大小,默认0.02
|
|
53
|
+
* @param {string} options.backgroundColor - 背景颜色,默认rgba(20, 30, 50, 0.9)
|
|
54
|
+
* @param {string} options.textColor - 文字颜色,默认#ffffff
|
|
55
|
+
* @param {string} options.borderColor - 边框颜色,默认#22d3ee
|
|
56
|
+
* @param {number} options.fontSize - 字体大小,默认16px
|
|
57
|
+
* @param {number} options.width - 标签宽度,默认auto
|
|
58
|
+
* @param {string} options.className - 自定义CSS类名
|
|
59
|
+
* @returns {CSS3DObject} 返回创建的3D标签对象,可以用于后续操作(如隐藏、删除等)
|
|
60
|
+
*/
|
|
61
|
+
create3DLabel(LabelID, contentHtml, position, options = {}, Events = {}) {
|
|
62
|
+
// 如果没有场景或标签渲染器,提示错误
|
|
63
|
+
if (!this.ThreeSdk.scene || !this.labelRenderer) {
|
|
64
|
+
console.error("❌ 场景未初始化,请确保 initMap() 已执行");
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 设置默认选项
|
|
69
|
+
const {
|
|
70
|
+
size = 0.02,
|
|
71
|
+
backgroundColor = "rgba(20, 30, 50, 0.9)",
|
|
72
|
+
textColor = "#ffffff",
|
|
73
|
+
borderColor = "#22d3ee",
|
|
74
|
+
fontSize = 16,
|
|
75
|
+
width = "auto",
|
|
76
|
+
className = "label-3d-custom",
|
|
77
|
+
} = options;
|
|
78
|
+
|
|
79
|
+
// 创建HTML元素
|
|
80
|
+
const labelDiv = document.createElement("div");
|
|
81
|
+
labelDiv.className = className;
|
|
82
|
+
|
|
83
|
+
// 设置样式(添加鼠标悬停效果和点击样式)
|
|
84
|
+
labelDiv.style.cssText = `
|
|
85
|
+
position: relative;
|
|
86
|
+
background: ${backgroundColor};
|
|
87
|
+
color: ${textColor};
|
|
88
|
+
padding: 12px 20px;
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
border: 2px solid ${borderColor};
|
|
91
|
+
box-shadow: 0 4px 12px rgba(34, 211, 238, 0.3);
|
|
92
|
+
font-size: ${fontSize}px;
|
|
93
|
+
font-weight: 500;
|
|
94
|
+
white-space: nowrap;
|
|
95
|
+
min-width: ${width};
|
|
96
|
+
text-align: center;
|
|
97
|
+
backdrop-filter: blur(10px);
|
|
98
|
+
pointer-events: auto; /* 允许标签接收鼠标事件 */
|
|
99
|
+
user-select: none;
|
|
100
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
|
101
|
+
cursor: pointer; /* 鼠标悬停时显示手型光标 */
|
|
102
|
+
transition: all 0.3s ease; /* 添加过渡动画 */
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
// 添加鼠标悬停效果
|
|
106
|
+
labelDiv.addEventListener("mouseenter", () => {
|
|
107
|
+
labelDiv.style.boxShadow = `0 6px 16px rgba(34, 211, 238, 0.5)`;
|
|
108
|
+
labelDiv.style.borderColor = "#60e5ff";
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
labelDiv.addEventListener("mouseleave", () => {
|
|
112
|
+
labelDiv.style.boxShadow = `0 4px 12px rgba(34, 211, 238, 0.3)`;
|
|
113
|
+
labelDiv.style.borderColor = borderColor;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 设置文本内容
|
|
117
|
+
labelDiv.innerHTML = contentHtml;
|
|
118
|
+
|
|
119
|
+
// 添加小箭头指示器指向下方
|
|
120
|
+
const arrow = document.createElement("div");
|
|
121
|
+
arrow.style.cssText = `
|
|
122
|
+
position: absolute;
|
|
123
|
+
bottom: -8px;
|
|
124
|
+
left: 50%;
|
|
125
|
+
transform: translateX(-50%);
|
|
126
|
+
width: 0;
|
|
127
|
+
height: 0;
|
|
128
|
+
border-left: 8px solid transparent;
|
|
129
|
+
border-right: 8px solid transparent;
|
|
130
|
+
border-top: 8px solid ${borderColor};
|
|
131
|
+
`;
|
|
132
|
+
labelDiv.appendChild(arrow);
|
|
133
|
+
|
|
134
|
+
// 创建 CSS3DObject
|
|
135
|
+
const label = new CSS3DObject(labelDiv);
|
|
136
|
+
|
|
137
|
+
// 设置位置
|
|
138
|
+
if (position instanceof THREE.Vector3) {
|
|
139
|
+
label.position.copy(position);
|
|
140
|
+
} else if (Array.isArray(position) && position.length >= 3) {
|
|
141
|
+
label.position.set(position[0], position[1], position[2]);
|
|
142
|
+
} else {
|
|
143
|
+
console.error("❌ 位置参数无效,请使用 THREE.Vector3 或 [x, y, z] 数组");
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 设置缩放(CSS3D默认很小,需要放大)
|
|
148
|
+
label.scale.set(size, size, size);
|
|
149
|
+
this.bindEvents(labelDiv, label, Events);
|
|
150
|
+
|
|
151
|
+
// 添加点击事件:点击标签后,相机移动到标签下方
|
|
152
|
+
// labelDiv.addEventListener("click", (e) => {
|
|
153
|
+
// e.stopPropagation(); // 阻止事件冒泡
|
|
154
|
+
// click(e);
|
|
155
|
+
// // this.moveCameraToLabel(label.position, {
|
|
156
|
+
// // distance: 0,
|
|
157
|
+
// // heightOffset: 20,
|
|
158
|
+
// // });
|
|
159
|
+
// });
|
|
160
|
+
|
|
161
|
+
console.log(
|
|
162
|
+
`✅ 3D标签已创建: "${contentHtml}" 位置: (${label.position.x}, ${label.position.y}, ${label.position.z})`
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
this._LabelMap.set(LabelID, label);
|
|
166
|
+
this.ThreeSdk.scene.add(label);
|
|
167
|
+
// 返回标签对象,方便后续操作
|
|
168
|
+
return label;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
bindEvents(labelDiv, label, Events = {}) {
|
|
172
|
+
const {
|
|
173
|
+
click = () => {},
|
|
174
|
+
mouseenter = () => {},
|
|
175
|
+
mouseleave = () => {},
|
|
176
|
+
} = Events;
|
|
177
|
+
labelDiv.addEventListener("click", (e) => {
|
|
178
|
+
e.stopPropagation(); // 阻止事件冒泡
|
|
179
|
+
click({
|
|
180
|
+
$event: e,
|
|
181
|
+
label: label,
|
|
182
|
+
labelDiv: labelDiv,
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
labelDiv.addEventListener("mouseenter", (e) => {
|
|
186
|
+
e.stopPropagation(); // 阻止事件冒泡
|
|
187
|
+
mouseenter({
|
|
188
|
+
$event: e,
|
|
189
|
+
label: label,
|
|
190
|
+
labelDiv: labelDiv,
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
labelDiv.addEventListener("mouseleave", (e) => {
|
|
194
|
+
e.stopPropagation(); // 阻止事件冒泡
|
|
195
|
+
mouseleave({
|
|
196
|
+
$event: e,
|
|
197
|
+
label: label,
|
|
198
|
+
labelDiv: labelDiv,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
|
|
3
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
4
|
+
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader.js";
|
|
5
|
+
import { MTLLoader } from "three/examples/jsm/loaders/MTLLoader.js";
|
|
6
|
+
import {
|
|
7
|
+
CSS3DRenderer,
|
|
8
|
+
CSS3DObject,
|
|
9
|
+
} from "three/examples/jsm/renderers/CSS3DRenderer.js";
|
|
10
|
+
import { ThreeMFLoader } from "three/examples/jsm/Addons.js";
|
|
11
|
+
|
|
12
|
+
export default class ModelControl {
|
|
13
|
+
_hoveredObject = null;
|
|
14
|
+
_clickableObjects = [];
|
|
15
|
+
_ModelMap = new Map();
|
|
16
|
+
constructor(container, ThreeSdk) {
|
|
17
|
+
this.container = container;
|
|
18
|
+
this.ThreeSdk = ThreeSdk;
|
|
19
|
+
this._init();
|
|
20
|
+
}
|
|
21
|
+
_init() {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 加载 GLTF 格式的建筑物模型
|
|
25
|
+
* @param {Object} options {ModelID, modelPath, position, scale, clickable}
|
|
26
|
+
* @param {string} options.ModelID 模型ID
|
|
27
|
+
* @param {string} options.modelPath 模型路径
|
|
28
|
+
* @param {Array} options.position 模型位置 [x, z]
|
|
29
|
+
* @param {number} options.scale 模型缩放
|
|
30
|
+
* @param {boolean} options.clickable 模型是否可点击
|
|
31
|
+
* @param {*} onLoadProcess 加载进度回调函数
|
|
32
|
+
* @returns
|
|
33
|
+
*/
|
|
34
|
+
loadGLTFBuilding(options, onLoadProcess) {
|
|
35
|
+
let {
|
|
36
|
+
ModelID,
|
|
37
|
+
modelPath,
|
|
38
|
+
position = [0, 0],
|
|
39
|
+
scale = 30,
|
|
40
|
+
clickable = true,
|
|
41
|
+
} = options;
|
|
42
|
+
return new Promise((res, rej) => {
|
|
43
|
+
if (this._ModelMap.has(ModelID)) {
|
|
44
|
+
res(this._ModelMap.get(ModelID));
|
|
45
|
+
}
|
|
46
|
+
console.log("开始加载 GLTF 建筑物模型...");
|
|
47
|
+
const gltfLoader = new GLTFLoader();
|
|
48
|
+
|
|
49
|
+
gltfLoader.load(
|
|
50
|
+
modelPath,
|
|
51
|
+
// 加载成功的回调
|
|
52
|
+
(gltf) => {
|
|
53
|
+
console.log("GLTF 建筑物模型加载成功!");
|
|
54
|
+
|
|
55
|
+
const model = gltf.scene;
|
|
56
|
+
|
|
57
|
+
// 设置阴影
|
|
58
|
+
model.traverse((child) => {
|
|
59
|
+
if (child.isMesh) {
|
|
60
|
+
// 🔑 关键修复:克隆材质,避免材质共享问题
|
|
61
|
+
// 如果不克隆,多个网格可能共享同一个材质引用,修改一个会影响所有
|
|
62
|
+
child.material = child.material.clone();
|
|
63
|
+
child.castShadow = true;
|
|
64
|
+
child.receiveShadow = true;
|
|
65
|
+
child.userData.clickable = clickable;
|
|
66
|
+
if (clickable) {
|
|
67
|
+
this._clickableObjects.push(child);
|
|
68
|
+
}
|
|
69
|
+
// 查找并保存水阀开关的引用
|
|
70
|
+
if (child.name === "Geom3D_4330") {
|
|
71
|
+
console.log("✅ 找到水阀开关: Geom3D_4330");
|
|
72
|
+
// 保存初始旋转状态
|
|
73
|
+
child.userData.initialRotation = child.rotation.clone();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 打印网格信息
|
|
77
|
+
console.log(`GLTF 网格: "${child.name}"`);
|
|
78
|
+
if (child.material) {
|
|
79
|
+
console.log(" 材质:", child.material.name || "未命名");
|
|
80
|
+
console.log(" 有贴图:", child.material.map ? "是" : "否");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 计算模型边界框
|
|
86
|
+
const box = new THREE.Box3().setFromObject(model);
|
|
87
|
+
const size = new THREE.Vector3();
|
|
88
|
+
box.getSize(size);
|
|
89
|
+
const minY = box.min.y;
|
|
90
|
+
|
|
91
|
+
// 缩放模型(根据需要调整)
|
|
92
|
+
const scaleFactor = scale;
|
|
93
|
+
model.scale.set(scaleFactor, scaleFactor, scaleFactor);
|
|
94
|
+
|
|
95
|
+
// 重新计算缩放后的边界框和中心
|
|
96
|
+
const boxScaled = new THREE.Box3().setFromObject(model);
|
|
97
|
+
const centerScaled = new THREE.Vector3();
|
|
98
|
+
boxScaled.getCenter(centerScaled);
|
|
99
|
+
const sizeScaled = new THREE.Vector3();
|
|
100
|
+
boxScaled.getSize(sizeScaled);
|
|
101
|
+
|
|
102
|
+
// 将模型居中:使模型的中心位于场景中心 (0, 0, 0)
|
|
103
|
+
// position[0] 和 position[1] 是用户想要模型中心的位置
|
|
104
|
+
// 但由于模型本身有中心偏移,需要减去 centerScaled 来补偿
|
|
105
|
+
model.position.x = position[0] - centerScaled.x; // 使模型中心在 position[0]
|
|
106
|
+
model.position.y = -minY * scaleFactor; // 放置在地面上
|
|
107
|
+
model.position.z = position[1] - centerScaled.z; // 使模型中心在 position[1]
|
|
108
|
+
|
|
109
|
+
// 相机始终看向场景中心 (0, 0, 0),而不是模型位置
|
|
110
|
+
// 这样地形(点阵底座)和模型都会在视野中,地形不会偏移
|
|
111
|
+
this.ThreeSdk.camera.lookAt(0, 0, 0);
|
|
112
|
+
|
|
113
|
+
// 添加到场景
|
|
114
|
+
this.ThreeSdk.scene.add(model);
|
|
115
|
+
|
|
116
|
+
console.log("GLTF 建筑物已添加到场景");
|
|
117
|
+
console.log(" 缩放:", scaleFactor);
|
|
118
|
+
console.log(
|
|
119
|
+
" 位置:",
|
|
120
|
+
model.position.x,
|
|
121
|
+
model.position.y,
|
|
122
|
+
model.position.z
|
|
123
|
+
);
|
|
124
|
+
console.log(" 原始大小:", size.x, "x", size.y, "x", size.z);
|
|
125
|
+
|
|
126
|
+
// 添加边界框辅助线(可选,用于调试)
|
|
127
|
+
const helper = new THREE.BoxHelper(model, 0x00ff00);
|
|
128
|
+
// scene.add(helper);
|
|
129
|
+
console.log("已添加绿色边界框辅助线");
|
|
130
|
+
|
|
131
|
+
this._ModelMap.set(ModelID, model);
|
|
132
|
+
res(model);
|
|
133
|
+
},
|
|
134
|
+
// 加载进度的回调
|
|
135
|
+
(xhr) => {
|
|
136
|
+
const percentComplete = (xhr.loaded / xhr.total) * 100;
|
|
137
|
+
console.log(`GLTF 建筑物加载进度: ${Math.round(percentComplete)}%`);
|
|
138
|
+
onLoadProcess && onLoadProcess(percentComplete);
|
|
139
|
+
},
|
|
140
|
+
// 加载错误的回调
|
|
141
|
+
(error) => {
|
|
142
|
+
console.error("加载 GLTF 建筑物时出错:", error);
|
|
143
|
+
rej(error);
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//开启模型鼠标移动事件监听 - 用于悬停效果
|
|
150
|
+
onModelMouseMoveListener(callback = () => {}) {
|
|
151
|
+
window.addEventListener("mousemove", (event) => {
|
|
152
|
+
let mouse = new THREE.Vector2();
|
|
153
|
+
// 计算鼠标在标准化设备坐标中的位置 (-1 到 +1)
|
|
154
|
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
155
|
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
156
|
+
|
|
157
|
+
// 更新射线
|
|
158
|
+
this.ThreeSdk.raycaster.setFromCamera(mouse, this.ThreeSdk.camera);
|
|
159
|
+
|
|
160
|
+
// 检测相交的对象
|
|
161
|
+
const intersects = this.ThreeSdk.raycaster.intersectObjects(
|
|
162
|
+
this._clickableObjects,
|
|
163
|
+
false
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (intersects.length > 0) {
|
|
167
|
+
const object = intersects[0].object;
|
|
168
|
+
|
|
169
|
+
// 如果是新的悬停对象
|
|
170
|
+
if (this._hoveredObject !== object) {
|
|
171
|
+
// 恢复之前悬停对象的材质
|
|
172
|
+
if (
|
|
173
|
+
this._hoveredObject &&
|
|
174
|
+
this._hoveredObject.userData.originalMaterial
|
|
175
|
+
) {
|
|
176
|
+
this._hoveredObject.material =
|
|
177
|
+
this._hoveredObject.userData.originalMaterial;
|
|
178
|
+
}
|
|
179
|
+
this._hoveredObject = object;
|
|
180
|
+
// 保存原始材质
|
|
181
|
+
|
|
182
|
+
if (!this._hoveredObject.userData.originalMaterial) {
|
|
183
|
+
this._hoveredObject.userData.originalMaterial =
|
|
184
|
+
this._hoveredObject.material.clone();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this._animateMouseMove(object);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
callback(object);
|
|
191
|
+
} else {
|
|
192
|
+
// 没有悬停对象,恢复状态
|
|
193
|
+
if (
|
|
194
|
+
this._hoveredObject &&
|
|
195
|
+
this._hoveredObject.userData.originalMaterial
|
|
196
|
+
) {
|
|
197
|
+
this._hoveredObject.material =
|
|
198
|
+
this._hoveredObject.userData.originalMaterial;
|
|
199
|
+
this._hoveredObject = null;
|
|
200
|
+
}
|
|
201
|
+
this.ThreeSdk.renderer.domElement.style.cursor = "default";
|
|
202
|
+
callback(null);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* 设置模型Mesh对象鼠标移动悬停效果
|
|
209
|
+
* @param {THREE.Mesh} object 模型Mesh对象
|
|
210
|
+
*/
|
|
211
|
+
_animateMouseMove(object) {
|
|
212
|
+
// 创建高亮材质
|
|
213
|
+
const highlightMaterial = this._hoveredObject.material.clone();
|
|
214
|
+
highlightMaterial.emissive = new THREE.Color(0x00ffff);
|
|
215
|
+
highlightMaterial.emissiveIntensity = 0.3;
|
|
216
|
+
this._hoveredObject.material = highlightMaterial;
|
|
217
|
+
|
|
218
|
+
// 更新鼠标样式和状态
|
|
219
|
+
this.ThreeSdk.renderer.domElement.style.cursor = "pointer";
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
//开启模型鼠标点击事件监听
|
|
223
|
+
onModelClickListener(callback = () => {}) {
|
|
224
|
+
window.addEventListener("click", (event) => {
|
|
225
|
+
let mouse = new THREE.Vector2();
|
|
226
|
+
// 计算鼠标位置
|
|
227
|
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
|
228
|
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
|
229
|
+
this.ThreeSdk.raycaster.setFromCamera(mouse, this.ThreeSdk.camera);
|
|
230
|
+
// 检测相交的对象
|
|
231
|
+
const intersects = this.ThreeSdk.raycaster.intersectObjects(
|
|
232
|
+
this._clickableObjects,
|
|
233
|
+
false
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (intersects.length > 0) {
|
|
237
|
+
const clickedObject = intersects[0].object;
|
|
238
|
+
const clickedPoint = intersects[0].point;
|
|
239
|
+
console.log("=== 点击事件触发 ===");
|
|
240
|
+
console.log(`点击对象: ${clickedObject.userData.name}`);
|
|
241
|
+
console.log(
|
|
242
|
+
`点击位置: (${clickedPoint.x.toFixed(2)}, ${clickedPoint.y.toFixed(
|
|
243
|
+
2
|
|
244
|
+
)}, ${clickedPoint.z.toFixed(2)})`
|
|
245
|
+
);
|
|
246
|
+
console.log(`对象信息:`, clickedObject.userData);
|
|
247
|
+
|
|
248
|
+
// 执行点击动画
|
|
249
|
+
this._animateClick(clickedObject);
|
|
250
|
+
|
|
251
|
+
callback(clickedObject);
|
|
252
|
+
} else {
|
|
253
|
+
callback(null);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 模型Mesh对象点击动画效果
|
|
260
|
+
* @param {THREE.Mesh} object 模型Mesh对象
|
|
261
|
+
*/
|
|
262
|
+
_animateClick(object) {
|
|
263
|
+
// 点击的网格的颜色永久变为绿色
|
|
264
|
+
object.material.color.set(0x00ff00);
|
|
265
|
+
const originalScale = object.scale.clone();
|
|
266
|
+
let progress = 0;
|
|
267
|
+
|
|
268
|
+
const animate = () => {
|
|
269
|
+
progress += 0.1;
|
|
270
|
+
const scale = 1 + Math.sin(progress * Math.PI) * 0.1;
|
|
271
|
+
object.scale.copy(originalScale).multiplyScalar(scale);
|
|
272
|
+
|
|
273
|
+
if (progress < 1) {
|
|
274
|
+
requestAnimationFrame(animate);
|
|
275
|
+
} else {
|
|
276
|
+
object.scale.copy(originalScale);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
animate();
|
|
280
|
+
}
|
|
281
|
+
}
|