From 120f31aa9ee23315e88b945a076fa724806de4cd Mon Sep 17 00:00:00 2001 From: nicomacbookpro <805879871@qq.com> Date: Wed, 20 Aug 2025 15:19:33 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=9C=E8=89=B2=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/three.js | 2497 ++++++++++++++-------------- src/views/bigScreen/home/index.vue | 1306 ++++++++------- 2 files changed, 1919 insertions(+), 1884 deletions(-) diff --git a/src/utils/three.js b/src/utils/three.js index 0070440..e833871 100644 --- a/src/utils/three.js +++ b/src/utils/three.js @@ -1,1244 +1,1253 @@ -// ========== 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: color, - 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); - } - 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 geometryColors = [ - "#f617d9", - "#779c6e", - "#3ee4bc", - "#ca41e7", - "#c88d38", - "#7467ef", - "#c7bef7", - ]; - 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: geometryColors[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: geometryColors[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); - -} - -/* ========== DEMO用例:直接渲染到页面的第一个div上 ========== */ -// const demo1 = drawAxes(document.querySelector("div"), {}); -// setInterval(()=>demo1.dispose(),2000); // 定时销毁示例(如需测试资源释放) +// ========== 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); + } + 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); + +} + +/* ========== DEMO用例:直接渲染到页面的第一个div上 ========== */ +// const demo1 = drawAxes(document.querySelector("div"), {}); +// setInterval(()=>demo1.dispose(),2000); // 定时销毁示例(如需测试资源释放) diff --git a/src/views/bigScreen/home/index.vue b/src/views/bigScreen/home/index.vue index 76b824b..68c35d9 100644 --- a/src/views/bigScreen/home/index.vue +++ b/src/views/bigScreen/home/index.vue @@ -1,640 +1,666 @@ - - - - - \ No newline at end of file + + + + +