Files
kdemo/src/utils/three.js
nicomacbookpro 00f40dba99 面板
2025-08-20 15:27:42 +08:00

1249 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ========== Three.js 相关模块引入 ==========
// 引入 Three.js 核心库及各类常用扩展
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; // 相机轨道控制器
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; // GLTF模型加载器
import { DRACOLoader } from "three/examples/jsm/loaders/DRACOLoader.js"; // Draco模型解压器
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader.js"; // HDR环境贴图加载器
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js"; // 后处理合成器
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js"; // 渲染通道
import { ShaderPass } from "three/examples/jsm/postprocessing/ShaderPass.js"; // 着色器通道
import { FXAAShader } from "three/examples/jsm/shaders/FXAAShader.js"; // 抗锯齿着色器
import { Water } from "three/examples/jsm/objects/Water.js"; // 水面对象
// Three.js 线框及文本相关扩展
import { LineSegments2 } from "three/examples/jsm/lines/LineSegments2.js";
import { LineSegmentsGeometry } from "three/examples/jsm/lines/LineSegmentsGeometry.js";
import { LineMaterial } from "three/examples/jsm/lines/LineMaterial.js";
import {
Line,
LineDashedMaterial,
BufferGeometry,
Float32BufferAttribute,
} from "three";
import { FontLoader } from "three/examples/jsm/loaders/FontLoader.js"; // 字体加载器
import { TextGeometry } from "three/examples/jsm/geometries/TextGeometry.js"; // 文本几何体
/* ========== 3条虚线辅助函数 ========== */
/**
* 为一个球体对象生成三条x、y、z方向虚线指示该点到对应坐标平面的投影。
* 每条虚线用LineDashedMaterial表示便于后续动态更新端点坐标。
* @param {Object3D} ball 目标球体
* @param {Scene} scene 所属场景
* @param {Object} opts 虚线样式参数
*/
function createDashedLinesForBall(
ball,
scene,
{ dashSize = 1, gapSize = 1, color = 0x7ec8ff, arrowSize = 1 } = {}
) {
const makeLine = (direction) => {
// 用 BufferGeometry+LineDashedMaterial 创建一条虚线
const geo = new BufferGeometry();
geo.setAttribute(
"position",
new Float32BufferAttribute(new Float32Array(6), 3)
); // 两端点(共6元素)
const mat = new LineDashedMaterial({
color,
dashSize,
gapSize,
transparent: true,
opacity: 0.6,
});
const line = new Line(geo, mat);
line.computeLineDistances();
scene.add(line);
// return line;
// 创建箭头
const dir = new THREE.Vector3();
// if (direction === "down") dir.set(0, -1, 0);
if (direction === "back") dir.set(0, 0, -1);
else if (direction === "left") dir.set(1, 0, 0);
if (direction != "down") {
const arrowHelper = new THREE.ArrowHelper(
dir,
new THREE.Vector3(),
0 * arrowSize, // 线段长度
color,
0.2 * arrowSize, // 箭头头部长度
0.1 * arrowSize // 箭头头部宽度
);
arrowHelper.line.material.transparent = true;
arrowHelper.line.material.opacity = 0.6;
// 设置箭头头部透明度
arrowHelper.cone.material = new THREE.MeshBasicMaterial({
color: "#00ff00",
transparent: true, // 必须设置为true
opacity: 0.6, // 与线段相同的透明度
});
scene.add(arrowHelper);
return { line, arrow: arrowHelper };
}
return { line };
};
// // 创建 z-y 面上的投影点
// const createZYPlaneProjection = () => {
// // 方法1使用圆形平面
// const geometry = new THREE.CircleGeometry(0.1 * arrowSize, 16);
// const material = new THREE.MeshBasicMaterial({
// color: color,
// transparent: true,
// opacity: 0.6,
// side: THREE.DoubleSide, // 确保两面可见
// });
// const circle = new THREE.Mesh(geometry, material);
// circle.position.copy(ball.position);
// circle.position.x = 0; // 放置在 x=0 平面
// circle.rotation.y = Math.PI / 2; // 旋转 90 度,使其平行于 z-y 平面
// scene.add(circle);
// return circle;
// // 方法2使用点精灵Points
// /*
// const geometry = new THREE.BufferGeometry();
// const vertices = new Float32Array([ball.position.x, ball.position.y, 0]);
// geometry.setAttribute('position', new THREE.BufferAttribute(vertices, 3));
// const material = new THREE.PointsMaterial({
// color: color,
// size: 0.2 * arrowSize,
// transparent: true,
// opacity: 0.6
// });
// const point = new THREE.Points(geometry, material);
// scene.add(point);
// return point;
// */
// };
const createZYPlaneProjection = () => {
const group = new THREE.Group(); // 用 Group 管理投影点和外圈
// 1. 核心投影点(实心圆)
const coreGeometry = new THREE.CircleGeometry(0.08 * arrowSize, 16);
const coreMaterial = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.8,
});
const core = new THREE.Mesh(coreGeometry, coreMaterial);
core.rotation.y = Math.PI / 2; // 平行于 z-y 平面
group.add(core);
// 2. 外圈淡化圆环(比核心稍大)
const ringGeometry = new THREE.RingGeometry(
0.08 * arrowSize, // 内半径(比核心稍大)
0.16 * arrowSize, // 外半径
32
);
const ringMaterial = new THREE.MeshBasicMaterial({
color: color,
transparent: true,
opacity: 0.3, // 更淡
side: THREE.DoubleSide,
});
const ring = new THREE.Mesh(ringGeometry, ringMaterial);
ring.rotation.y = Math.PI / 2; // 平行于 z-y 平面
group.add(ring);
// 初始位置与小球的 x 相同(不再固定 x=0
group.position.copy(ball.position);
group.position.x = 0;
scene.add(group);
return group; // 返回整个组
};
// 记录在球体的 userData 里,便于后续动画时更新
ball.userData.dashedLines = {
down: makeLine("down"), // y方向到底面
back: makeLine("back"), // z方向到后面
left: makeLine("left"), // x方向到侧面
zPlaneProjection: createZYPlaneProjection(), // z-y面投影点
};
}
function updateDashedLines(ball) {
const { dashedLines } = ball.userData;
const ballPos = ball.position;
const radius = ball.geometry.parameters.radius;
// // 更新下方向虚线及箭头
// updateLineAndArrow(
// dashedLines.down,
// [ballPos.x, ballPos.y, ballPos.z],
// [ballPos.x, 0, ballPos.z], // 假设地面在y=0
// radius
// );
// 更新后方向虚线及箭头
updateLineAndArrow(
dashedLines.back,
[ballPos.x, ballPos.y, ballPos.z - 1],
[ballPos.x, ballPos.y, 0], // 假设后面在z=0
radius
);
// 更新左方向虚线及箭头
updateLineAndArrow(
dashedLines.left,
[ballPos.x, ballPos.y, ballPos.z],
[0, ballPos.y, ballPos.z], // 假设左面在x=0
radius
);
}
function updateLineAndArrow(lineObj, start, end, radius) {
// 更新虚线
const positions = lineObj.line.geometry.attributes.position.array;
positions[0] = start[0];
positions[1] = start[1];
positions[2] = start[2];
positions[3] = end[0];
positions[4] = end[1];
positions[5] = end[2];
lineObj.line.geometry.attributes.position.needsUpdate = true;
lineObj.line.computeLineDistances();
// 计算中点位置(从球表面开始)
const midPoint = [
start[0] + (end[0] - start[0]) * 0.5,
start[1] + (end[1] - start[1]) * 0.5,
start[2] + (end[2] - start[2]) * 0.5,
];
// 调整箭头位置,使其从球表面开始
const dir = new THREE.Vector3(
end[0] - start[0],
end[1] - start[1],
end[2] - start[2]
).normalize();
// 箭头位置从中点稍微向球偏移一点,确保在虚线中间
lineObj.arrow.position.set(
midPoint[0] - dir.x * radius * 0.5,
midPoint[1] - dir.y * radius * 0.5,
midPoint[2] - dir.z * radius * 0.5
);
}
/* ========== 创建自定义网格线(抗锯齿) ========== */
/**
* 构建网格线条对象,可用于三维空间的不同面,支持自定义分段数与线宽。
* @param {number} w 网格宽度
* @param {number} h 网格高度
* @param {number} divX x方向分段数
* @param {number} divY y方向分段数
* @param {color} color 网格线颜色
* @param {number} px 线宽(单位像素)
* @returns {LineSegments2} 线段对象
*/
function createCustomGridAA(w, h, divX, divY, color = 0xffffff, px = 1.5) {
// 按分段数生成所有线的端点坐标
const pos = [];
const stepX = w / divX,
stepY = h / divY;
for (let i = 0; i <= divX; i++) {
const x = i * stepX;
pos.push(x, 0, 0, x, h, 0); // 垂直线
}
for (let j = 0; j <= divY; j++) {
const y = j * stepY;
pos.push(0, y, 0, w, y, 0); // 水平线
}
// 线条材质用LineMaterial支持屏幕分辨率感知线宽不随缩放变化
const geo = new LineSegmentsGeometry().setPositions(pos);
const mat = new LineMaterial({
color,
linewidth: px,
worldUnits: false,
transparent: true,
opacity: 0.5,
});
mat.resolution.set(window.innerWidth, window.innerHeight);
return new LineSegments2(geo, mat);
}
/* ========== 坐标轴刻度文本构建 ========== */
/**
* 创建带刻度的向量轴标签(支持中文自动换行)
* @param {Object} 配置参数
* axis: 轴方向('x'/'y'/'z'
* length: 轴总长度
* labels: 自定义标签数组(可选)
* divisions: 分段数量
* fontUrl: 字体文件路径(支持中文)
* size: 文字大小
* height: 文字厚度
* color: 文字颜色
* colors: 按索引指定颜色数组
* offset: 位置偏移量
* parent: 父容器(必须)
* opacity: 透明度
* maxWidth: 单行最大宽度默认10
* lineHeight: 行间距倍数默认1.2
*/
function buildVectorTicks({
axis = "x",
length = 50,
labels = null,
divisions = 5,
fontUrl = "/fonts/FangSong_Regular.json", // 确保字体支持中文
size = 2,
height = 0.2,
color = 0xffffff,
colors = [],
offset = new THREE.Vector3(),
parent = null,
opacity = 0.8,
maxWidth = 10, // 新增:单行最大宽度
lineHeight = 1.2, // 新增:行高倍数
} = {}) {
if (!parent) {
console.warn("未指定父容器,无法创建刻度标签");
return;
}
new FontLoader().load(fontUrl, (font) => {
const group = new THREE.Group();
parent.add(group);
const arr = labels ?? Array.from({ length: divisions + 1 }, (_, i) => i.toString());
let step
if (axis === "y") {
step = length / (arr.length || 1);
} else {
step = length / (arr.length - 1 || 1);
}
/**
* 中文换行处理(按字符而非英文单词)
* @param {string} text 待处理文本
* @returns {string[]} 分割后的行数组最多3行
*/
const wrapChineseText = (text) => {
const chars = text.split(''); // 按字符分割(适用于中文)
const lines = [];
let currentLine = '';
for (const char of chars) {
const testLine = currentLine + char;
const tempGeo = new TextGeometry(testLine, { font, size, height: 0 });
tempGeo.computeBoundingBox();
const testWidth = tempGeo.boundingBox.max.x - tempGeo.boundingBox.min.x;
if (axis === "y") {
currentLine = testLine;
} else {
if (testWidth <= maxWidth) {
currentLine = testLine;
} else {
lines.push(currentLine);
currentLine = char;
if (lines.length >= 2) break; // 限制最多3行
}
}
}
if (currentLine) lines.push(currentLine);
return lines.slice(0, 3);
};
// 创建所有标签
arr.forEach((text, i) => {
const lines = wrapChineseText(text);
const material = new THREE.MeshBasicMaterial({
color: colors[i] || color, // 优先使用colors数组中的指定颜色
toneMapped: false,
transparent: true,
opacity,
});
// 每个标签的容器(用于多行文本)
const labelGroup = new THREE.Group();
// 创建每行文本
lines.forEach((line, lineIndex) => {
const geo = new TextGeometry(line, {
font,
size,
height,
curveSegments: 12, // 提高曲线分段数使中文更平滑
});
geo.computeBoundingBox();
geo.center();
const mesh = new THREE.Mesh(geo, material);
if (axis != "y") {
// 垂直偏移计算(首行在上,向下排列)
const verticalOffset =
((lines.length - 1) * size * lineHeight) / 2 -
lineIndex * size * lineHeight;
mesh.position.y = verticalOffset;
}
labelGroup.add(mesh);
});
// 设置标签组位置
const position = i * step;
if (axis === "x") {
labelGroup.rotation.y = Math.PI / 2; // X轴标签旋转90度
labelGroup.position.set(offset.x, offset.y, offset.z + position);
} else if (axis === "y") {
labelGroup.rotation.y = Math.PI / 4; // Y轴标签旋转45度
labelGroup.position.set(offset.x, offset.y - position, offset.z);
} else {
labelGroup.position.set(offset.x + position, offset.y, offset.z); // Z轴
}
group.add(labelGroup);
});
});
}
/* ========== Draco解压和GLTF加载器 ========== */
const dracoLoader = new DRACOLoader().setDecoderPath("/draco/"); // 设置Draco解码器路径
const gltfLoader = new GLTFLoader().setDRACOLoader(dracoLoader); // GLTF加载器关联Draco
/* ========== 主函数:画三维坐标轴及交互 ========== */
/**
* 在传入的DOM元素内创建完整的三维坐标系、网格面、刻度、球体模型、交互、后处理、UI按钮等。
* @param {Element} element 渲染容器
* @param {Object} options 配置参数
* @returns {Object} {scene, camera, renderer, dispose}
*/
export function drawAxes(element, options = {}, ballCallBack) {
// ========== 配置参数及默认值 ==========
const {
background = "#171717", // 背景色
gridSize = { x: 50, y: 50, z: 50 }, // 三维网格尺寸
// grid_uv_up = [5, 5],
// grid_uv_down = [5, 5],
grid_uv_left = [5, 5],
// grid_uv_right = [5, 4],
// grid_uv_front = [5, 4],
grid_uv_back = [5, 5],
show_water = true, // 是否显示水面
water_height = 5, // 水面厚度
data = {},
labels_left = ["", "A", "B", "C", "D", "E"],
labels_back = ["", "A", "B", "C", "D", "E"],
labels_height = [],
ztObj = {},
} = options;
/* ========== 场景/相机/渲染器初始化 ========== */
const scene = new THREE.Scene();
// scene.background = new THREE.Color(background);
scene.position.set(-gridSize.x / 2, -gridSize.y / 2, -gridSize.z / 2); // 修改 场景 中心点
const camera = new THREE.PerspectiveCamera(
75,
element.clientWidth / element.clientHeight,
0.1,
2000
);
camera.position.set(gridSize.x, gridSize.y, gridSize.z); // 俯视+侧视角
const renderer = new THREE.WebGLRenderer({
alpha: true, // 启用透明度
antialias: true,
});
const dpr = Math.min(window.devicePixelRatio || 1, 2);
renderer.setPixelRatio(dpr);
renderer.setSize(element.clientWidth, element.clientHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
element.appendChild(renderer.domElement);
// 轨道控制器(支持旋转/缩放/平移相机)
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.minPolarAngle = 0;
controls.maxPolarAngle = Math.PI / 2.8;
/* ========== 后处理抗锯齿 ========== */
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
const fxaa = new ShaderPass(FXAAShader);
const updateFXAA = () =>
fxaa.material.uniforms.resolution.value.set(
1 / (element.clientWidth * dpr),
1 / (element.clientHeight * dpr)
);
updateFXAA();
composer.addPass(fxaa);
/* ========== 环境光照/HDR环境贴图 ========== */
new RGBELoader().load("/hdr/basic.hdr", (tex) => {
tex.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = tex;
});
/* ========== 网格和水面生成 ========== */
const { x: sizeX, y: sizeY, z: sizeZ } = gridSize;
const gridGroup = new THREE.Group();
scene.add(gridGroup);
scene.userData.waterList = [];
// 可选:底部加一块水面(水面动画来自 Water.js
if (show_water) {
// const light = new THREE.DirectionalLight(0xffffbb, 1);
// light.position.set(-1, 1, -1);
// scene.add(light);
const waterNormals = new THREE.TextureLoader().load(
"/textures/water_1.png",
(t) => {
t.wrapS = t.wrapT = THREE.RepeatWrapping;
}
);
const water = new Water(new THREE.BoxGeometry(sizeX, water_height, sizeZ), {
// 纹理大小(影响波纹细节)
textureWidth: 2048, // 可以增加到2048获取更高细节
textureHeight: 2048,
// 法线贴图(控制波纹样式)
waterNormals: waterNormals,
// 太阳方向(影响反光方向)
sunDirection: new THREE.Vector3(1, 1, 1).normalize(),
// 颜色相关
sunColor: 0xffffff, // 阳光颜色(高光部分)
// sunColor: 0xff0326, // 阳光颜色(高光部分)
// waterColor: 0x1e90ff, // 水体基础颜色
waterColor: 0x034b88, // 水体基础颜色
// waterColor: 0x001e0f, // 水体基础颜色
distortionScale: 30, // 波纹强度0.5-5之间调整distortionScale: 100.0,
// 其他效果
fog: scene.fog !== undefined, // 是否受雾效影响
alpha: 0.1, // 透明度0-1
// time: 0, // 可用于手动控制动画时间
});
water.position.set(sizeX / 2, -water_height / 2, sizeZ / 2);
gridGroup.add(water);
scene.userData.waterList.push(water);
}
// 根据配置添加每个面上的网格线
// const color = 0xffffff, // 0x244b78 2d578a
const color = 0x2d578a,
px = 1.0;
if (typeof grid_uv_down !== "undefined" && grid_uv_down) {
// 底面
const g = createCustomGridAA(sizeX, sizeZ, ...grid_uv_down, color, px);
g.rotation.x = Math.PI / 2;
g.position.set(0, 0, 0);
gridGroup.add(g);
}
if (typeof grid_uv_left !== "undefined" && grid_uv_left) {
// 左侧
const g = createCustomGridAA(sizeZ, sizeY, ...grid_uv_left, color, px);
g.rotation.y = -Math.PI / 2;
g.position.set(0, 0, 0);
gridGroup.add(g);
}
// if (typeof grid_uv_right !== "undefined" && grid_uv_right) {
// // 右侧
// const g = createCustomGridAA(sizeZ, sizeY, ...grid_uv_right, color, px);
// g.rotation.y = -Math.PI / 2;
// g.position.set(sizeX, 0, 0);
// gridGroup.add(g);
// }
// if (typeof grid_uv_front !== "undefined" && grid_uv_front) {
// // 前侧
// const g = createCustomGridAA(sizeX, sizeY, ...grid_uv_front, color, px);
// g.position.set(0, 0, sizeZ);
// gridGroup.add(g);
// }
// 后侧 using
let gris_back_g = null;
if (typeof grid_uv_back !== "undefined" && grid_uv_back) {
gris_back_g = createCustomGridAA(sizeX, sizeY, ...grid_uv_back, color, px);
gris_back_g.position.set(0, 0, 0);
gridGroup.add(gris_back_g);
}
// X轴、Z轴上的刻度文本
buildVectorTicks({
axis: "x",
length: sizeZ,
labels: labels_left,
offset: new THREE.Vector3(0, sizeY + 5, 0),
parent: gridGroup,
});
buildVectorTicks({
axis: "z",
length: sizeX,
labels: labels_back,
offset: new THREE.Vector3(0, sizeY + 5, 0),
parent: gridGroup,
});
buildVectorTicks({
axis: "y",
length: sizeY,
labels: labels_height,
divisions: 10, // 0xf617d9, 0x779c6e, 0x3ee4bc, 0xca41e7, 0xc88d38, 0x7467ef, 0xc7bef7,
colors: [
0xf617d9, 0x779c6e, 0x3ee4bc, 0xca41e7, 0xc88d38, 0x7467ef, 0xc7bef7,
],
offset: new THREE.Vector3(0, sizeY, 0),
parent: gridGroup,
opacity: 1,
});
buildVectorTicks({
axis: "z", // x 轴
size: 2.5,
maxWidth: 15,
length: sizeX,
labels: ["信宿"],
offset: new THREE.Vector3(sizeX + 15, 0, 0),
parent: gridGroup,
});
buildVectorTicks({
axis: "y", // y 轴
size: 2.5,
maxWidth: 15,
length: sizeY,
labels: ["信息类型"],
offset: new THREE.Vector3(0, sizeY + 15, 0),
parent: gridGroup,
});
buildVectorTicks({
axis: "x", // z 轴
size: 2.5,
maxWidth: 15,
length: sizeZ,
labels: ["信源"],
offset: new THREE.Vector3(0, 0, sizeZ + 15),
parent: gridGroup,
});
addPositiveAxes(scene, sizeX, sizeY, sizeZ);
/* ========== 球体模型批量加载和放置 ========== */
// 四种球用同一个glb分别用不同位置数组、父分组存储
let styleGroups = {};
data.forEach((el) => {
styleGroups[el.name] = null;
});
styleGroups["灰球"] = null;
const lineGeometryColors = [
"#0x00ff00",
"#779c6e",
"#3ee4bc",
"#ca41e7",
"#c88d38",
"#7467ef",
"#c7bef7",
];
const geometryColors = [
"#888888",
"#888888",
"#888888",
"#888888",
"#888888",
"#888888",
"#888888",
];
const ballScene = new THREE.Group();
geometryColors.forEach((el) => {
// 几何体
const geometry = new THREE.SphereGeometry(1, 64, 64);
// 材质重点metalness 和 roughness
const material = new THREE.MeshStandardMaterial({
color: el, // 基础颜色
metalness: 0.2, // 金属度
roughness: 0.8, // 光滑度(数值越低越光滑)
});
//网孔(Mesh)是用来承载几何模型的一个对象,可以把材料应用到它上面
const ball = new THREE.Mesh(geometry, material);
ball.color = el;
ballScene.children.push(ball);
});
const geometry = new THREE.SphereGeometry(1, 64, 64);
// 材质重点metalness 和 roughness
const material = new THREE.MeshStandardMaterial({
color: "#898184", // 基础颜色
metalness: 0.2, // 金属度
roughness: 0.8, // 光滑度(数值越低越光滑)
});
const greyBall = new THREE.Mesh(geometry, material); // 灰色小球
const allBalls = []; // 所有点位对象
const greyGroup = new THREE.Group();
scene.add(greyGroup);
styleGroups["灰球"] = greyGroup;
styleGroups["灰球"].name = "灰球";
styleGroups["灰球"].color = "#898184";
const cloneBall = (idx, list, key, name) => {
if (!list) return;
const g = new THREE.Group();
scene.add(g);
styleGroups[key] = g;
styleGroups[key].name = name;
styleGroups[key].color = ballScene.children[idx].color;
list.forEach((point) => {
// 每个球体用 glb 里不同索引的 mesh clone 出来
// const n = root.children[idx].clone(true);
let n = ballScene.children[idx].clone(true);
// console.log(ztObj, "ztObj");
if (ztObj && ztObj[point.id] == false) {
n = greyBall.clone(true);
} else {
if (point.fourLinks == false) {
n = greyBall.clone(true);
}
}
n.pointId = point.id;
const p = point.pointData;
n.position.set(...p);
n.userData.originalPos = new THREE.Vector3(...p); // 原始位置
n.userData.groupKey = `${p[0]}_${p[1]}`; // 分组唯一key
n.userData.groupKeyXZ = `${p[0]}_${p[2]}`; // 分组唯一key 向x-z面收缩合并
g.add(n);
if (ztObj && ztObj[point.id] == false) {
greyGroup.add(n);
} else {
if (point.fourLinks == false) {
greyGroup.add(n);
}
}
allBalls.push(n);
if (p[0] != 0 && p[1] != 0 && p[2] != 0) {
createDashedLinesForBall(n, scene, {
dashSize: 1,
gapSize: 1,
color: lineGeometryColors[idx],
arrowSize: 8,
}); // 给每个球加三条投影虚线
updateDashedLines(n);
}
});
};
data.forEach((el, index) => {
cloneBall(index, el.point, el.name, el.name);
});
// 添加单个点位的方法
function addSinglePoint(parent, position, modelIndex, groupName) {
// const ball = root.children[modelIndex].clone(true);
const ball = ballScene.children[modelIndex].clone(true);
ball.pointId = position.id;
const point = position.pointData;
ball.position.set(...point);
ball.userData = {
originalPos: new THREE.Vector3(...point),
groupKey: `${point[0]}_${point[1]}`,
groupKeyXZ: `${point[0]}_${point[2]}`,
groupName: groupName,
};
parent.add(ball);
allBalls.push(ball);
if (point[0] != 0 && point[1] != 0 && point[2] != 0) {
createDashedLinesForBall(ball, scene, {
dashSize: 1,
gapSize: 1,
color: lineGeometryColors[modelIndex],
});
updateDashedLines(ball);
}
return ball;
}
/**
* 移除单个点位
* @param {THREE.Object3D} pointObj 要移除的点位对象
* @param {boolean} removeFromGroup 是否从所属组中移除默认true
*/
function removeSinglePoint(pointObj, removeFromGroup = true) {
if (!pointObj || !allBalls.includes(pointObj)) return;
// 移除虚线
if (pointObj.userData?.dashedLines) {
Object.values(pointObj.userData.dashedLines).forEach((line) => {
line.geometry.dispose();
line.material.dispose();
scene.remove(line);
});
}
// 从allBalls中移除
const index = allBalls.indexOf(pointObj);
if (index !== -1) {
allBalls.splice(index, 1);
}
// 从父组中移除
if (removeFromGroup && pointObj.parent) {
pointObj.parent.remove(pointObj);
}
// 如果所属组为空,移除整个组
const groupName = pointObj.userData?.groupName;
if (
groupName &&
styleGroups[groupName] &&
styleGroups[groupName].children.length === 0
) {
removeGroup(groupName);
}
}
/**
* 通过ID或位置查找点位
* @param {string} groupName 组名
* @param {Array} position 位置 [x,y,z]
* @returns {THREE.Object3D|null} 找到的点位对象
*/
function getPointById(groupName, position) {
if (!styleGroups[groupName]) return null;
const pos = new THREE.Vector3(...position);
const tolerance = 0.001; // 位置匹配容差
for (const ball of styleGroups[groupName].children) {
if (ball.position.distanceTo(pos) < tolerance) {
return ball;
}
}
return null;
}
/* ========== 交互处理:射线检测吸附与复位 ========== */
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hovered = null;
// 组吸附与目标映射
const activeGroups = new Set(); // 当前已吸附的分组key
const groupTargets = new Map(); // 分组key -> 吸附目标位置Vector3
const activeGroupsXZ = new Set(); // 当前已吸附的分组key xz水平面组
const groupTargetsXZ = new Map(); // 分组key xz -> 吸附目标位置Vector3
// 鼠标移动时,更新 mouse 向量归一化NDC坐标
function onMouseMove(e) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
}
window.addEventListener("mousemove", onMouseMove);
// 鼠标点击时判断是否有球在back面被点中并吸附/释放
function onClick($event) {
raycaster.setFromCamera(mouse, camera);
const inter = raycaster.intersectObjects(allBalls);
if (inter.length === 0) {
if ($event.target === renderer.domElement) {
// 确认点击的是Three.js的canvas
ballCallBack(false, {});
}
return;
}
const obj = inter[0].object;
// 只允许在z约等于0back面的球体才可点击
if (Math.abs(obj.position.z) > 1e-3) {
ballCallBack(true, obj);
return;
}
// if (Math.abs(obj.position.z) > 1e-3) return;
const key = obj.userData.groupKey;
// console.log(obj.userData, "obj.userData");
if (activeGroups.has(key)) {
// 若已吸附,则取消吸附(恢复原位)
activeGroups.delete(key);
groupTargets.delete(key);
} else {
// 未吸附则将目标z设为0吸附到back面
activeGroups.add(key);
groupTargets.set(
key,
new THREE.Vector3(obj.position.x, obj.position.y, 0)
);
}
}
window.addEventListener("click", onClick);
let isCollapsedXY = false; // 点位小球是否处于回收状态
let isCollapsedXZ = false; // 点位小球是否处于回收状态
function setCollapsedXY(params) {
if (isCollapsedXZ) return;
isCollapsedXY = !isCollapsedXY; // 切换状态
allBalls.forEach((ball) => {
const key = ball.userData.groupKey;
if (isCollapsedXY) {
// // 回收状态吸附到X-Y平面z=0
groupTargets.set(
key,
new THREE.Vector3(ball.position.x, ball.position.y, 0)
);
activeGroups.add(key);
} else {
// 恢复状态:回到原始位置
activeGroups.delete(key);
groupTargets.delete(key);
}
});
}
function setCollapsedXZ(params) {
if (isCollapsedXY) return;
isCollapsedXZ = !isCollapsedXZ; // 切换状态
allBalls.forEach((ball) => {
const key = ball.userData.groupKeyXZ; // groupKeyXZ
if (isCollapsedXZ) {
// // 回收状态吸附到X-Y平面z=0
groupTargetsXZ.set(
key,
new THREE.Vector3(ball.position.x, 0, ball.position.z)
);
activeGroupsXZ.add(key);
} else {
// 恢复状态:回到原始位置
activeGroupsXZ.delete(key);
groupTargetsXZ.delete(key);
}
});
}
/* ========== 视窗尺寸自适应处理 ========== */
const onResize = () => {
const w = element.clientWidth,
h = element.clientHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setPixelRatio(dpr);
renderer.setSize(w, h);
composer.setSize(w, h);
updateFXAA();
// 需同步网格线材质分辨率,否则线宽异常
gridGroup.traverse((o) => {
if (o.material?.isLineMaterial) o.material.resolution.set(w, h);
});
};
window.addEventListener("resize", onResize);
/* ========== 动画循环与实时状态刷新 ========== */
let rafId;
const animate = () => {
rafId = requestAnimationFrame(animate);
controls.update();
// 水面动画推动uniform的time
scene.userData.waterList.forEach(
(w) => (w.material.uniforms.time.value += 1 / 60)
);
// 鼠标悬浮放大效果
raycaster.setFromCamera(mouse, camera);
const inter = raycaster.intersectObjects(allBalls);
if (inter.length) {
if (hovered !== inter[0].object) {
hovered?.scale.set(1, 1, 1);
hovered = inter[0].object;
hovered.scale.set(1.2, 1.2, 1.2);
}
} else {
hovered?.scale.set(1, 1, 1);
hovered = null;
}
// 球体吸附z=0/复位动画插值
allBalls.forEach((obj) => {
const key = obj.userData.groupKey;
const keyXZ = obj.userData.groupKeyXZ;
if (activeGroups.has(key)) {
obj.position.lerp(groupTargets.get(key), 0.05); // 吸附到指定目标点
}
if (activeGroupsXZ.has(keyXZ)) {
obj.position.lerp(groupTargetsXZ.get(keyXZ), 0.05); // 吸附到指定目标点
}
if (!activeGroups.has(key) && !activeGroupsXZ.has(keyXZ)) {
obj.position.lerp(obj.userData.originalPos, 0.05); // 回到原始位置
}
});
// 虚线端点动态刷新(始终连接球体当前坐标与投影点)
allBalls.forEach((obj) => {
const lines = obj.userData.dashedLines;
// console.log(lines, "lineslineslineslines");
if (!lines) return;
const p = obj.position;
// y方向down
{
const arr = lines.down.line.geometry.attributes.position.array;
arr[0] = p.x;
arr[1] = p.y;
arr[2] = p.z;
arr[3] = p.x;
arr[4] = 0;
arr[5] = p.z;
lines.down.line.geometry.attributes.position.needsUpdate = true;
lines.down.line.computeLineDistances();
}
// z方向back
{
const arr = lines.back.line.geometry.attributes.position.array;
arr[0] = p.x;
arr[1] = p.y;
arr[2] = p.z;
arr[3] = p.x;
arr[4] = p.y;
arr[5] = 0;
lines.back.line.geometry.attributes.position.needsUpdate = true;
lines.back.line.computeLineDistances();
}
// x方向left
{
const arr = lines.left.line.geometry.attributes.position.array;
arr[0] = p.x;
arr[1] = p.y;
arr[2] = p.z;
arr[3] = 0;
arr[4] = p.y;
arr[5] = p.z;
lines.left.line.geometry.attributes.position.needsUpdate = true;
lines.left.line.computeLineDistances();
}
});
allBalls.forEach((obj) => {
const lines = obj.userData.dashedLines;
if (!lines) return;
// console.log(lines, "lines");
const { zPlaneProjection } = obj.userData.dashedLines;
if (!zPlaneProjection) return;
const p = obj.position;
// 完全跟随小球的 x 和 yz 固定为原始值,假设投影在 z=0 平面)
zPlaneProjection.position.x = 0; // 跟随 x 移动
zPlaneProjection.position.y = p.y; // 跟随 y 移动
zPlaneProjection.position.z = p.z; // 固定 z=0或其他平面
updateDashedLines(obj);
});
composer.render();
};
animate();
/* ========== 销毁/资源回收接口 ========== */
function dispose() {
cancelAnimationFrame(rafId);
window.removeEventListener("resize", onResize);
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("click", onClick);
controls.dispose();
// 回收所有球的虚线资源
allBalls.forEach((obj) => {
const lines = obj.userData.dashedLines;
if (!lines) return;
// // 检查是否存在投影数据
// if (!obj.userData.dashedLines) return;
// const { down, back, left, zPlaneProjection } = obj.userData.dashedLines;
// // 销毁虚线
// if (down?.line) {
// scene.remove(down.line);
// down.line.geometry.dispose();
// down.line.material.dispose();
// }
// // 销毁箭头back 和 left 方向)
// [back, left].forEach((dir) => {
// if (dir?.line) {
// scene.remove(dir.line);
// dir.line.geometry.dispose();
// dir.line.material.dispose();
// }
// if (dir?.arrow) {
// scene.remove(dir.arrow);
// dir.arrow.cone.geometry.dispose();
// dir.arrow.cone.material.dispose();
// dir.arrow.line.geometry.dispose();
// dir.arrow.line.material.dispose();
// }
// });
// // 销毁 z-y 平面投影(可能是 Group 或 Points
// if (zPlaneProjection) {
// scene.remove(zPlaneProjection);
// // 如果是 Group包含圆和圆环
// if (zPlaneProjection instanceof THREE.Group) {
// zPlaneProjection.children.forEach((child) => {
// child.geometry.dispose();
// child.material.dispose();
// });
// }
// // 如果是 Points点精灵
// else if (zPlaneProjection instanceof THREE.Points) {
// zPlaneProjection.geometry.dispose();
// zPlaneProjection.material.dispose();
// }
// }
// // 清除 userData 中的引用
// delete ball.userData.dashedLines;
Object.values(lines).forEach((l) => {
// 销毁虚线
if (l?.line) {
scene.remove(l.line);
l.line.geometry.dispose();
l.line.material.dispose();
if (l?.arrow) {
scene.remove(l.arrow);
l.arrow.cone.geometry.dispose();
l.arrow.cone.material.dispose();
l.arrow.line.geometry.dispose();
l.arrow.line.material.dispose();
}
}
// 销毁 z-y 平面投影(可能是 Group 或 Points
// 如果是 Group包含圆和圆环
if (l instanceof THREE.Group) {
l.children.forEach((child) => {
child.geometry.dispose();
child.material.dispose();
});
}
scene.remove(l);
});
});
// 场景资源释放
scene.traverse((o) => {
o.geometry?.dispose();
(Array.isArray(o.material) ? o.material : [o.material]).forEach((m) =>
m?.dispose()
);
o.texture?.dispose?.();
});
composer.dispose();
renderer.dispose();
element.removeChild(renderer.domElement);
// element.removeChild(btnContainer);
}
// 隐藏/显示 点位小球、虚线、箭头、映射
function setGroupVisible(groupName, visible) {
if (styleGroups[groupName]) {
// 设置组内所有小球的可见性
styleGroups[groupName].visible = visible;
// 遍历组内所有小球,设置对应的虚线、箭头和映射的可见性
styleGroups[groupName].children.forEach((ball) => {
const lines = ball.userData.dashedLines;
if (!lines) return;
// 设置虚线、箭头和映射的可见性
Object.values(lines).forEach((lineObj) => {
if (lineObj?.line) lineObj.line.visible = visible;
if (lineObj?.arrow) lineObj.arrow.visible = visible;
});
// 设置 z-y 平面投影的可见性
if (lines.zPlaneProjection) {
lines.zPlaneProjection.visible = visible;
}
});
}
}
return {
scene,
camera,
renderer,
dispose, // 销毁 模型
getGroups: () => styleGroups, // 获取 模型 分组点位
collapsedXY: () => {
return setCollapsedXY();
},
collapsedXZ: () => {
return setCollapsedXZ();
},
// 添加 模型 分组点位
addPoint: (groupName, position, modelIndex) => {
if (!styleGroups[groupName]) {
styleGroups[groupName] = new THREE.Group();
scene.add(styleGroups[groupName]);
styleGroups[groupName].name = groupName;
}
return addSinglePoint(
styleGroups[groupName],
position,
modelIndex,
groupName
);
},
removePoint: removeSinglePoint, // 动态移除点位
getPointById: getPointById, // 获取点位对象
// 模型 分组点位 现隐
setGroupVisible: (groupName, visible) => {
return setGroupVisible(groupName, visible);
},
};
}
// 创建坐标轴
async function addPositiveAxes(scene, sizeX, sizeY, sizeZ) {
// 创建X轴红色只显示正向
const xAxis = new THREE.ArrowHelper(
new THREE.Vector3(1, 0, 0), // 方向
new THREE.Vector3(0, 0, 0), // 起点
sizeX + 10, // 长度
0xf6b501, // 颜色(黄色)
2, // 头部长度
1 // 头部宽度
);
scene.add(xAxis);
// 创建Y轴绿色只显示正向
const yAxis = new THREE.ArrowHelper(
new THREE.Vector3(0, 1, 0),
new THREE.Vector3(0, 0, 0),
sizeY + 10,
0xd9ebff, // 颜色(色)
2,
1
);
scene.add(yAxis);
// 创建Z轴蓝色只显示正向
const zAxis = new THREE.ArrowHelper(
new THREE.Vector3(0, 0, 1),
new THREE.Vector3(0, 0, 0),
sizeZ + 10,
0x88f0ee, // 颜色(蓝色)
2,
1
);
scene.add(zAxis);
}