import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; import html2canvas from 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.esm.js'; const interfaceUrl = 'http://127.0.0.1:5000'; const uploadFiles = document.getElementById('uploadFiles'); const uploadBtn = document.getElementById('uploadBtn'); const serverFilesDiv = document.getElementById('serverFiles'); let startClick = true; let isMulFiles = false; // 目录选择 & 列表渲染 uploadBtn.addEventListener('click', async ()=>{ const files = Array.from(uploadFiles.files); if(!files.length) return alert('请选择要上传的文件'); const fd = new FormData(); files.forEach(f=>fd.append('files',f,f.name)); const res = await fetch(interfaceUrl+'/upload_data_files',{method:'POST',body:fd}); if(!res.ok) { const e=await res.json(); return alert('上传失败:'+e.error); } alert('上传成功'); loadServerFiles(); }); async function loadServerFiles(){ serverFilesDiv.innerHTML = ''; const res = await fetch(interfaceUrl+'/list_data_files'); const { files } = await res.json(); files.forEach(fn=>{ const cb=document.createElement('input'); cb.type='checkbox';cb.value=fn;cb.id='sf_'+fn; const lbl=document.createElement('label'); lbl.htmlFor='sf_'+fn;lbl.textContent=fn; const row=document.createElement('div'); row.style.display='flex';row.style.alignItems='center';row.style.marginBottom='4px'; lbl.style.marginLeft='6px';row.append(cb,lbl); serverFilesDiv.appendChild(row); }); } loadServerFiles(); // 不再硬编码 baseDate,由 UI 输入驱动 let baseDate = new Date(); const dateInput = document.getElementById('baseDate'); const timeInput = document.getElementById('baseTime'); function updateBaseDate() { const d = dateInput.value; // YYYY-MM-DD const t = timeInput.value; // HH:MM:SS baseDate = new Date(`${d}T${t}`); } function getBounds() { const xMinI = document.getElementById('x-min'), xMaxI = document.getElementById('x-max'), yMinI = document.getElementById('y-min'), yMaxI = document.getElementById('y-max'), zMinI = document.getElementById('z-min'), zMaxI = document.getElementById('z-max'); return { x: [ Number(xMinI.value), Number(xMaxI.value) ], y: [ Number(yMinI.value), Number(yMaxI.value) ], z: [ Number(zMinI.value), Number(zMaxI.value) ] }; } dateInput.addEventListener('change', updateBaseDate); timeInput.addEventListener('change', updateBaseDate); updateBaseDate(); let scene, camera, renderer, controls, currentGroup, dataGroup, axisGroup, labelRenderer = null; let timeMin = 0, timeMax = 1, playIntervalSec = 600; let isPlaying = false, frameIdx = 0; const coldMap = ['#800080', '#0000ff', '#00ffff']; const warmMap = ['#00ff00', '#ffff00', '#ff7f00', '#ff0000']; function lerpColor(arr, t) { const n = arr.length, s = t * (n - 1), i = Math.floor(s), f = s - i; if (i >= n - 1) return new THREE.Color(arr[n - 1]); return new THREE.Color(arr[i]).lerp(new THREE.Color(arr[i + 1]), f); } function getHeatColor(t) { return t <= 0.4 ? lerpColor(coldMap, t / 0.4) : lerpColor(warmMap, (t - 0.4) / 0.6); } initScene(); initControls(); // 获取时间间隔(分钟) function getIntervalMin() { return Number(document.getElementById('intervalMin').value); } function refreshTimeRange() { // 获取 time_range,转换为分钟并四舍五入 fetch(interfaceUrl + '/time_range') .then(r => r.json()) .then(({ timeMin: secMin, timeMax: secMax }) => { const minM = Math.round(secMin / 60); const maxM = Math.round(secMax / 60); timeMin = minM; timeMax = maxM; const st = document.getElementById('startTime'), et = document.getElementById('endTime'); st.min = et.min = minM; st.max = et.max = maxM; st.value = minM; et.value = maxM; updateTimeLabel(st.value); }); } refreshTimeRange(); function initScene() { const C = document.getElementById('container'); scene = new THREE.Scene(); scene.rotateX(-Math.PI / 2); scene.background = new THREE.Color(0xffffff); camera = new THREE.PerspectiveCamera(50, C.clientWidth / C.clientHeight, 0.1, 5000); camera.position.set(200, 200, -1600); renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true }); renderer.setSize(C.clientWidth, C.clientHeight); C.appendChild(renderer.domElement); // 光照 scene.add(new THREE.AmbientLight(0xffffff, 0.8)); const dirLight = new THREE.DirectionalLight(0xffffff, 0.8); dirLight.position.set(200, 300, 200); dirLight.castShadow = true; scene.add(dirLight); const fillLight = new THREE.DirectionalLight(0xffffff, 0.4); fillLight.position.set(-200, -100, -200); scene.add(fillLight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; // CSS2DRenderer for axis labels labelRenderer = new CSS2DRenderer(); labelRenderer.setSize(C.clientWidth, C.clientHeight); labelRenderer.domElement.style.position = 'absolute'; labelRenderer.domElement.style.top = '0'; labelRenderer.domElement.style.pointerEvents = 'none'; C.appendChild(labelRenderer.domElement); // OrbitControls controls = new OrbitControls(camera, renderer.domElement); controls.target.set(150, 100, -80); controls.enableDamping = false; controls.dampingFactor = 0.05; controls.enablePan = false; // 支持平移 let userInteracting = false; controls.addEventListener('start', () => userInteracting = true); controls.addEventListener('end', () => userInteracting = false); controls.addEventListener('change', () => { if (!userInteracting) return; const az = THREE.MathUtils.radToDeg(controls.getAzimuthalAngle()); const po = THREE.MathUtils.radToDeg(controls.getPolarAngle()); document.getElementById('azimuth').value = az.toFixed(0); document.getElementById('polar').value = po.toFixed(0); }); window.addEventListener('resize', () => { camera.aspect = C.clientWidth / C.clientHeight; camera.updateProjectionMatrix(); renderer.setSize(C.clientWidth, C.clientHeight); labelRenderer.setSize(C.clientWidth, C.clientHeight); }); // 2) 井轨迹 fetch(interfaceUrl + '/get_well_path') .then(r => r.json()) .then(({ points }) => { // 1) 把后端返回的点转成 Vector3 数组 const verts = points.map(p => new THREE.Vector3(p.x, p.y, p.z)); // 2) 构造 BufferGeometry const geo = new THREE.BufferGeometry().setFromPoints(verts); // 3) LineBasicMaterial 支持一个简单的 linewidth const mat = new THREE.LineBasicMaterial({ color: 0xff8800, linewidth: 3, // 粗细(注意:大多数浏览器/平台只支持 1px) }); // 4) THREE.Line 或者 THREE.LineSegments(如果想把每段分离)都可以用 const line = new THREE.Line(geo, mat); line.renderOrder = 1; // 确保它按这个顺序绘制 // 5) 直接添加到场景,按世界坐标就能穿透等值面 scene.add(line); }); // 射孔簇 fetch(interfaceUrl + '/get_perforations') .then(r => r.json()) .then(({ points }) => { const geo = new THREE.BufferGeometry(); const pos = new Float32Array(points.length * 3); points.forEach((p, i) => { pos[3*i] = p.x; pos[3*i+1] = p.y; pos[3*i+2] = p.z; }); geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); const mat = new THREE.PointsMaterial({ color: 0xffff00, size: 25, sizeAttenuation: true, depthWrite: false, depthTest: false }); const perfs = new THREE.Points(geo, mat); perfs.renderOrder = 200; // 最后画 scene.add(perfs); }); // 桥塞 fetch(interfaceUrl + '/get_plugs') .then(r => r.json()) .then(({ points }) => { const geo = new THREE.BufferGeometry(); const pos = new Float32Array(points.length * 3); points.forEach((p, i) => { pos[3*i] = p.x; pos[3*i+1] = p.y; pos[3*i+2] = p.z; }); geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); const mat = new THREE.PointsMaterial({ color: 0xff0000, size: 25, sizeAttenuation: true, depthWrite: false, depthTest: false }); const plugs = new THREE.Points(geo, mat); plugs.renderOrder = 210; // 桥塞后画 scene.add(plugs); }); // 层位标记 fetch(interfaceUrl + '/get_cells') .then(r => r.json()) .then(({ points }) => { // 1) 构造所有点的 BufferGeometry const geo = new THREE.BufferGeometry(); const pos = new Float32Array(points.length * 3); points.forEach((p, i) => { pos[3*i] = p.x; pos[3*i+1] = p.y; pos[3*i+2] = p.z; }); geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)); // 2) 绘制所有点 const mat = new THREE.PointsMaterial({ color: 'blue', size: 20, sizeAttenuation: true, depthWrite: false, depthTest: false }); const pointCloud = new THREE.Points(geo, mat); pointCloud.renderOrder = 212; scene.add(pointCloud); // 3) 针对每个点,添加一个 CSS2D 标签显示它的 name points.forEach(p => { const span = document.createElement('span'); span.className = 'cell-label'; span.textContent = p.name; Object.assign(span.style, { fontSize: '12px', color: '#e82121', background: 'transparent', // 或者小半透明背景 padding: '0 2px', // 左右留点空白 lineHeight: '1em', whiteSpace: 'nowrap', pointerEvents: 'none', display: 'inline-block' // 让 span 能设置宽高、padding }); // 用 span 生成标签对象 const label = new CSS2DObject(span); label.position.set(p.x, p.y, p.z + 3); label.renderOrder = 213; scene.add(label); }); }); axisGroup = new THREE.Group(); dataGroup = new THREE.Group(); // 画坐标轴一次 drawAxes(); scene.add(axisGroup); // 把 dataGroup 一开始也加进去(里面现在空) scene.add(dataGroup); // render loop (function animate() { requestAnimationFrame(animate); // 每帧都重新应用用户锁定的中心点 controls.update(); renderer.render(scene, camera); labelRenderer.render(scene, camera); })(); // initial zoom-in function zoomByScroll(ticks, factorPerTick = 0.8) { const dir = new THREE.Vector3().subVectors(camera.position, controls.target); dir.multiplyScalar(Math.pow(factorPerTick, ticks)); camera.position.copy(controls.target).add(dir); camera.updateProjectionMatrix(); controls.update(); } zoomByScroll(8, 0.99); } function drawAxes() { if (axisGroup) { scene.remove(axisGroup); // (可选:彻底 dispose 三维对象的 geometry/material) axisGroup.traverse(o => { if (o.geometry) o.geometry.dispose(); if (o.material) o.material.dispose(); }); } // —— ② 把之前留下的 CSS2D 标签清空 —— labelRenderer.domElement.innerHTML = ''; // —— ③ 新建一个全新的 axisGroup —— axisGroup = new THREE.Group(); const { x: [xmin, xmax], y: [ymin, ymax], z: [zmin, zmax] } = getBounds(); console.log("x:" + xmin); const lenX = xmax - xmin, lenY = ymax - ymin, lenZ = zmax - zmin; const ticksMap = { X: Math.ceil(lenX / 50), Y: Math.ceil(lenY / 50), Z: Math.ceil(lenZ / 50) }; const tickesNameMap = {X: 'X', Y: 'Y', Z: 'Z'}; // 通用刻度文字样式 const LABEL_STYLE = { fontSize: '12px', color: '#000', // 黑色 margin: '0px', padding: '0px', whiteSpace: 'nowrap', }; // 画单条轴及其刻度和标签 [['X', xmin, lenX, 'x', 0xff0000], ['Y', ymin, lenY, 'y', 0x00ff00], ['Z', zmin, lenZ, 'z', 0x0000ff] ].forEach(([name, start, length, axis, color]) => { const mat = new THREE.LineBasicMaterial({ color }); // 计算轴起点 const origin = new THREE.Vector3( axis === 'x' ? start : (axis === 'y' ? xmax : xmin), axis === 'y' ? start : ymin, axis === 'z' ? start : zmin ); // 计算轴方向向量 const dir = new THREE.Vector3( axis === 'x' ? 1 : 0, axis === 'y' ? 1 : 0, axis === 'z' ? 1 : 0 ); // —— 绘制轴主线 —— const lineGeo = new THREE.BufferGeometry().setFromPoints([ origin, origin.clone().add(dir.clone().multiplyScalar(length)) ]); axisGroup.add(new THREE.Line(lineGeo, mat)); // 计算刻度外推方向(针对不同轴分别处理) let offsetDir; // Vector3(x,y,z) if (axis === 'x') offsetDir = new THREE.Vector3(0, 0.8, -0.8); // X 轴刻度文字向下 else if (axis === 'y') offsetDir = new THREE.Vector3(0.5, 0, -0.1); // Y 轴刻度文字向前 else offsetDir = new THREE.Vector3(1, 0, 0); // Z 轴刻度文字向左 const labelOffset = offsetDir.multiplyScalar(10); // —— 绘制刻度线和数字标签 —— const ticks = ticksMap[name]; for (let i = 0; i <= ticks; i++) { const t = i / ticks; const pos = origin.clone().add(dir.clone().multiplyScalar(length * t)); // 刻度线 const mkGeo = new THREE.BufferGeometry().setFromPoints([ pos.clone().add(offsetDir.clone().multiplyScalar(0.5)), pos.clone().add(offsetDir.clone().multiplyScalar(-0.5)) ]); axisGroup.add(new THREE.Line(mkGeo, mat)); // 数字标签 const val = (start + length * t).toFixed(0); const div = document.createElement('div'); Object.assign(div.style, LABEL_STYLE); div.textContent = val; const label = new CSS2DObject(div); label.position.copy(pos.clone().add(labelOffset)); axisGroup.add(label); } // —— 绘制轴标题 —— const titleDiv = document.createElement('div'); Object.assign(titleDiv.style, LABEL_STYLE, { fontWeight: 'bold' }); titleDiv.textContent = tickesNameMap[name]; const titleLabel = new CSS2DObject(titleDiv); let worldPos; if (axis === 'x') { worldPos = origin.clone() .add(dir.clone().multiplyScalar(length * 0.5)) .add(offsetDir.clone().multiplyScalar(length * 0.008)); } else if (axis === 'y') { worldPos = origin.clone() .add(dir.clone().multiplyScalar(length * 0.5)) .add(offsetDir.clone().multiplyScalar(length * 0.018)); } else { worldPos = origin.clone() .add(dir.clone().multiplyScalar(length * 0.5)) .add(offsetDir.clone().multiplyScalar(length * -0.018)); } titleLabel.position.copy(worldPos); axisGroup.add(titleLabel); }); // —— 手工生成 XY/XZ/YZ 三个平面上的网格 —— const gridMat = new THREE.LineBasicMaterial({ color: 0xcccccc, opacity: 0.6, transparent: true }); function makePlaneGrid(fixedAxis, fixedValue, axis1, range1, div1, axis2, range2, div2) { const geom = new THREE.BufferGeometry(); const pos = []; const step1 = (range1[1] - range1[0]) / div1; const step2 = (range2[1] - range2[0]) / div2; // 沿 axis1 方向的线 for (let j = 0; j <= div2; j++) { const c2 = range2[0] + step2 * j; const p1 = {}, p2 = {}; p1[fixedAxis] = fixedValue; p2[fixedAxis] = fixedValue; p1[axis2] = c2; p2[axis2] = c2; p1[axis1] = range1[0]; p2[axis1] = range1[1]; pos.push(p1.x||0, p1.y||0, p1.z||0, p2.x||0, p2.y||0, p2.z||0); } // 沿 axis2 方向的线 for (let i = 0; i <= div1; i++) { const c1 = range1[0] + step1 * i; const q1 = {}, q2 = {}; q1[fixedAxis] = fixedValue; q2[fixedAxis] = fixedValue; q1[axis1] = c1; q2[axis1] = c1; q1[axis2] = range2[0]; q2[axis2] = range2[1]; pos.push(q1.x||0, q1.y||0, q1.z||0, q2.x||0, q2.y||0, q2.z||0); } geom.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3)); return new THREE.LineSegments(geom, gridMat); } // XY 平面: z = zmin axisGroup.add(makePlaneGrid('z', zmin, 'x', [xmin, xmax], ticksMap.X, 'y', [ymin, ymax], ticksMap.Y)); // XZ 平面: y = ymin axisGroup.add(makePlaneGrid('y', ymax, 'x', [xmin, xmax], ticksMap.X, 'z', [zmin, zmax], ticksMap.Z)); // YZ 平面: x = xmin axisGroup.add(makePlaneGrid('x', xmin, 'z', [zmin, zmax], ticksMap.Z, 'y', [ymin, ymax], ticksMap.Y)); // 加入场景 scene.add(axisGroup); } function applyView() { if(!currentGroup) return; const sel = document.getElementById('viewSelect').value; const box = new THREE.Box3().setFromObject(currentGroup); const center = box.getCenter(new THREE.Vector3()); const d = box.getSize(new THREE.Vector3()).length() * 1.2; let pos; switch(sel){ case '主视图': pos = new THREE.Vector3(0,0,d); break; case '左视图': pos = new THREE.Vector3(-d,0,0); break; case '右视图': pos = new THREE.Vector3(d,0,0); break; case '俯视图': pos = new THREE.Vector3(0,d,0); break; default: pos = new THREE.Vector3(d,d,d); break; } camera.position.copy(center.clone().add(pos)); controls.target.copy(center); camera.lookAt(center); controls.update(); console.log("切换视图:" + sel); } function initControls() { // 底层按钮和输入绑定 document.getElementById('apply').onclick = renderSimple; document.getElementById('play').onclick = startPlay; document.getElementById('stop').onclick = stopPlay; document.getElementById('record').onclick = startRecording; document.getElementById('startTime').addEventListener('input', () => { let st = +startTime.value; if (st < timeMin) st = timeMin; if (st > timeMax) st = timeMax; startTime.value = st; document.getElementById('timeLabel').innerText = st; }); // 限制 endTime 在 [startTime, timeMax] document.getElementById('endTime').addEventListener('input', () => { let et = +endTime.value, st = +startTime.value; if (et < st) et = st; if (et > timeMax) et = timeMax; endTime.value = et; }); viewSelect.addEventListener('change', () => { // 确保 currentGroup 有值 currentGroup = currentGroup || dataGroup; applyView(); }); } function getStartEndInput() { return { start: +document.getElementById('startTime').value, end: +document.getElementById('endTime').value }; } function getCurrentLabel() { return +document.getElementById('timeLabel').innerText || 0; } function updateTimeLabel(timeValue) { const labelEl = document.getElementById('timeLabel'); // 取当前文本,尝试解析为浮点数;如果不是数字(包括空字符串),则默认 0 const current = parseFloat(labelEl.innerText) || 0; const updated = current + timeValue; if (updated > timeMax) { updated = timeMax; } labelEl.innerText = updated; } function checkFileStatus() { const selected = Array.from( document.querySelectorAll('#serverFiles input:checked') ).map(cb => cb.value); if (!selected.length) { return alert('请先勾选要渲染的文件'); } console.log("文件数量:" + selected.length); if (selected.length > 1) { console.log("多文件"); isMulFiles = true; } return selected; } // 简单渲染 async function renderSimple() { startClick = true; const selected = checkFileStatus(); // 1) 读取所有输入参数 let { start, end } = getStartEndInput(); // 3) 清空旧的数据 Group(但保留坐标轴) dataGroup.clear(); // 4) 对每个文件,拉数据并生成一组等值面,添加到 dataGroup await renderFrame(start, end); } // 播放:start 固定,endLabel 动态 async function startPlay() { startClick = true; const selected = checkFileStatus(); // 3) 清空旧的数据 Group(但保留坐标轴) dataGroup.clear(); const { start, end } = getStartEndInput(); let current = start; document.getElementById('timeLabel').innerText = current; isPlaying = true; play.disabled = true; const interval = getIntervalMin(); // 分钟 while (isPlaying && current <= end) { current += interval; // 4) 对每个文件,拉数据并生成一组等值面,添加到 dataGroup await renderFrame(start, current); document.getElementById('timeLabel').innerText = current; } stopPlay(); } function stopPlay() { isPlaying = false; play.disabled = false; alert("结束"); } async function savePng() { // —— 用 WebGL + CSS2D canvas 合成一张图 renderer.render(scene, camera); labelRenderer.render(scene, camera); // 拿 WebGL 图 const webglDataURL = renderer.domElement.toDataURL('image/png'); const webglImg = new Image(); webglImg.src = webglDataURL; await new Promise(r => webglImg.onload = r); // 拿标签层(CSS2DRenderer)——这里还是用 html2canvas,只拍 labels div const labelShot = await html2canvas(labelRenderer.domElement, { backgroundColor: null, useCORS: true, allowTaint: true, width: renderer.domElement.width, height: renderer.domElement.height }); // 合并 const w = renderer.domElement.width, h = renderer.domElement.height; const combo = document.createElement('canvas'); combo.width = w; combo.height = h; const ctx = combo.getContext('2d'); ctx.drawImage(webglImg, 0, 0, w, h); ctx.drawImage(labelShot, 0, 0, w, h); // 画左上角的时间和体积(用白字,无背景) const timeText = document.getElementById('timeStamp').textContent; const volumeText = document.getElementById('volume').textContent; ctx.font = '16px sans-serif'; ctx.fillStyle = '#000'; ctx.fillText(timeText, 10, 25); // 调整 y 使文字在合适位置 ctx.fillText(volumeText, 10, 45); // 上传 const png = combo.toDataURL('image/png'); await fetch(interfaceUrl + '/save_frame', { method: 'POST', headers: { 'Content-Type':'application/json' }, body: JSON.stringify({ idx: frameIdx++, image: png }) }); } async function downloadGIF() { // 合成 GIF 并下载 const res = await fetch(interfaceUrl + '/make_gif'); const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `animation_${Date.now()}.gif`; document.body.appendChild(a); a.click(); URL.revokeObjectURL(url); } // 录制:逻辑跟播放一样,每帧多存图 async function startRecording() { startClick = true; const selected = checkFileStatus(); // 3) 清空旧的数据 Group(但保留坐标轴) dataGroup.clear(); const { start, end } = getStartEndInput(); let current = start; document.getElementById('timeLabel').innerText = current; isPlaying = true; frameIdx = 0; await fetch(interfaceUrl + '/init_frames', { method: 'POST' }); const interval = getIntervalMin(); while (isPlaying && current <= end) { current += interval; await renderFrame(start, current); await savePng(); document.getElementById('timeLabel').innerText = current; } downloadGIF(); stopPlay(); } // —— 渲染单帧到 dataGroup(保持不变,只做数据 & Three.js 渲染)—— async function renderFrame(startTime, endTime) { // 1) 读取所有输入参数 const levels = +document.getElementById('levels').value; const gridRes = +document.getElementById('gridRes').value; const tau = +document.getElementById('tau').value; const S1 = +document.getElementById('S1').value, R1 = +document.getElementById('R1').value; const S2 = +document.getElementById('S2').value, R2 = +document.getElementById('R2').value; const S3 = +document.getElementById('S3').value, R3 = +document.getElementById('R3').value; const opacity = +document.getElementById('meshOpacity').value; // 2) 构造 URL,只针对传入的那些文件 const url = new URL(interfaceUrl + '/get_meshes'); url.searchParams.set('levels', levels); url.searchParams.set('energy_thresh', tau); url.searchParams.set('grid_res', gridRes); url.searchParams.set('S1', S1); url.searchParams.set('R1', R1); url.searchParams.set('S2', S2); url.searchParams.set('R2', R2); url.searchParams.set('S3', S3); url.searchParams.set('R3', R3); url.searchParams.set('startTime', startTime * 60); url.searchParams.set('endTime', endTime * 60); // 前端时间输入是分钟,需要转换成秒再传给后端 const timeLabelVal = getCurrentLabel() * 60; updateBaseDate(); // 保证读到最新输入 const actualDate = new Date(baseDate.getTime() + timeLabelVal * 1000); const hh = String(actualDate.getHours()).padStart(2, '0'); const mm = String(actualDate.getMinutes()).padStart(2, '0'); const ss = String(actualDate.getSeconds()).padStart(2, '0'); document.getElementById('timeStamp').innerText = `${actualDate.getFullYear()}-${(actualDate.getMonth() + 1).toString().padStart(2, '0')}-${actualDate.getDate().toString().padStart(2, '0')} ${hh}:${mm}:${ss}`; const selected = checkFileStatus(); let volumeTotal = 0; for (let i = 0; i < selected.length; i++) { url.searchParams.set('file', selected[i]); // 3) 拿数据并渲染到 dataGroup const res = await fetch(url); const { meshes, bounds, volume } = await res.json(); volumeTotal += volume; document.getElementById('volume').innerText = '体积:' + volumeTotal + '立方米'; const group = new THREE.Group(); group.position.set(bounds.x[0], bounds.y[0], bounds.z[0]); for (let j = 0; j < meshes.length; j++) { const m = meshes[j]; const geom = new THREE.BufferGeometry(); geom.setAttribute( 'position', new THREE.BufferAttribute(new Float32Array(m.vertices.flat()), 3) ); geom.setIndex(new THREE.BufferAttribute( new Uint32Array(m.faces.flat()), 1 )); geom.computeVertexNormals(); const mat = new THREE.MeshPhongMaterial({ color: getHeatColor(j/(meshes.length-1)), transparent: true, opacity, side: THREE.DoubleSide, shininess: 14, specular: 0x888888, depthWrite: false, depthTest: true }); const mesh = new THREE.Mesh(geom, mat); mesh.renderOrder = j; group.add(mesh); } if (i === 0) { // 第一次才清,第二个及以后直接叠加 dataGroup.clear(); } dataGroup.add(group); } // 4) 第一次聚焦:如果不 “锁定坐标轴” const fixAxes = document.getElementById('fixAxes').checked; if (startClick && !fixAxes) { drawAxes(); const box = new THREE.Box3().setFromObject(dataGroup); const ctr = box.getCenter(new THREE.Vector3()); controls.target.copy(ctr); camera.position.set( ctr.x, ctr.y, ctr.z + box.getSize(new THREE.Vector3()).length() ); controls.update(); } startClick = false; }