You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
earthquake_3d_viewer_front/static/js/iso_scatter.js

788 lines
28 KiB
JavaScript

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;
}