// ========== 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); }