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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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