1249 lines
39 KiB
JavaScript
1249 lines
39 KiB
JavaScript
// ========== 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约等于0(back面)的球体才可点击
|
||
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 和 y(z 固定为原始值,假设投影在 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);
|
||
|
||
}
|