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

798 lines
29 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');
// 目录选择 & 列表渲染
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 });
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 }) => {
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: 'blue',
size: 20,
sizeAttenuation: true,
depthWrite: false,
depthTest: false
});
const perfs = new THREE.Points(geo, mat);
perfs.renderOrder = 212; // 最后画
scene.add(perfs);
});
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);
// drawAxes();
}
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();
}
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;
});
document.getElementById('viewSelect').onchange = 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;
}
// 简单渲染
async function renderSimple() {
// 1) 读取所有输入参数
let { start, end } = getStartEndInput();
const startSec = start * 60;
const endSec = end * 60;
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) 找到所有被勾选的文件
const selected = Array.from(
document.querySelectorAll('#serverFiles input:checked')
).map(cb => cb.value);
if (!selected.length) {
return alert('请先勾选要渲染的文件');
}
// 3) 清空旧的数据 Group但保留坐标轴
dataGroup.clear();
// 4) 对每个文件,拉数据并生成一组等值面,添加到 dataGroup
for (const fn of selected) {
const url = new URL(interfaceUrl + '/get_meshes');
url.searchParams.set('file', fn);
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', startSec);
url.searchParams.set('endTime', endSec);
const res = await fetch(url);
const { meshes, bounds, volume } = await res.json();
// (可选)更新总体积或显示最后一个文件的体积
document.getElementById('volume').innerText = '体积:' + volume + '立方米';
// 构造一个小组来承载当前文件所有层
const group = new THREE.Group();
group.position.set(bounds.x[0], bounds.y[0], bounds.z[0]);
for (let i = 0; i < meshes.length; i++) {
const m = meshes[i];
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(i / (meshes.length - 1)),
transparent: true,
opacity: opacity,
side: THREE.DoubleSide,
shininess: 14,
specular: 0x888888,
depthWrite: false,
depthTest: true
});
const mesh = new THREE.Mesh(geom, mat);
mesh.renderOrder = i;
group.add(mesh);
}
// 把这个文件的等值面组加入 dataGroup
dataGroup.add(group);
}
// 5) 重新聚焦到所有数据的中央(可选)
const box = new THREE.Box3().setFromObject(dataGroup);
const center = box.getCenter(new THREE.Vector3());
controls.target.copy(center);
camera.position.set(center.x, center.y, center.z + box.getSize(new THREE.Vector3()).length());
controls.update();
}
// 播放start 固定endLabel 动态
async function startPlay() {
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;
await renderFrame(start, current);
document.getElementById('timeLabel').innerText = current;
}
stopPlay();
}
function stopPlay() {
isPlaying = false;
play.disabled = false;
alert("结束");
}
// 录制:逻辑跟播放一样,每帧多存图
async function startRecording() {
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);
// 截图上传
const shot = await html2canvas(container, { backgroundColor: null, useCORS: true });
const png = shot.toDataURL();
await fetch(interfaceUrl + '/save_frame', {
method: 'POST',
headers: { 'Content-Type':'application/json' },
body: JSON.stringify({ idx: frameIdx++, image: png })
});
document.getElementById('timeLabel').innerText = current;
}
// 合成 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);
stopPlay();
}
let initialFocused = false;
async function renderFrame(startTime, endTime) {
console.log("处理范围:" + startTime + "," + endTime);
startTime = startTime * 60;
endTime = endTime * 60;
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;
// 前端时间输入是分钟,需要转换成秒再传给后端
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 url = `${interfaceUrl}/get_meshes?levels=${levels}`
+ `&energy_thresh=${tau}&grid_res=${gridRes}`
+ `&S1=${S1}&R1=${R1}&S2=${S2}&R2=${R2}&S3=${S3}&R3=${R3}`
+ `&startTime=${startTime}&endTime=${endTime}`;
const { meshes, bounds, volume } = await (await fetch(url)).json();
document.getElementById('volume').innerText = '体积:' + volume + '立方米';
const newG = new THREE.Group();
newG.position.set(bounds.x[0], bounds.y[0], bounds.z[0]);
for (let i = 0; i < meshes.length; i++) {
const m = meshes[i];
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 iso = i / (levels - 1);
const mat = new THREE.MeshPhongMaterial({
color: getHeatColor(iso),
transparent: true,
side: THREE.DoubleSide,
opacity: opacity,
shininess: 14, // 控制高光粗细
specular: 0x888888, // 高光颜色
depthWrite: false,
depthTest: true
});
const mesh = new THREE.Mesh(geom, mat);
// mesh.renderOrder = meshes.length - i;
mesh.renderOrder = i;
newG.add(mesh);
}
scene.add(newG);
if (currentGroup) {
scene.remove(currentGroup);
currentGroup.traverse(o => o.geometry && o.geometry.dispose());
}
currentGroup = newG;
// 仅第一次渲染时对焦
if (!initialFocused) {
const box = new THREE.Box3().setFromObject(currentGroup);
const ctr = box.getCenter(new THREE.Vector3());
controls.target.copy(ctr);
camera.position.set(ctr.x, ctr.y, ctr.z);
controls.update();
initialFocused = true;
}
drawAxes();
// 后续帧只更新 mesh不触碰 camera.target
controls.update();
renderer.render(scene, camera);
labelRenderer.render(scene, camera);
applyView();
// applyAngles();
}
// 新增:根据两个输入框的度数,计算摄像机球坐标
function applyAngles() {
// 1) 解析当前角度输入
const azDeg = Number(document.getElementById('azimuth').value);
const poDeg = Number(document.getElementById('polar').value);
const az = THREE.MathUtils.degToRad(azDeg);
const po = THREE.MathUtils.degToRad(poDeg);
// 2) 取出并更新 OrbitControls 自己维护的 target
const target = controls.target; // **不要** clone()
// 3) 球面半径:相机到 target 的当前距离(保持不变)
const radius = camera.position.distanceTo(target);
// 4) 用球坐标公式计算新的相机位置
const sinP = Math.sin(po);
const cosP = Math.cos(po);
const sinA = Math.sin(az);
const cosA = Math.cos(az);
camera.position.set(
target.x + radius * sinP * cosA,
target.y + radius * cosP,
target.z + radius * sinP * sinA
);
// 5) 让相机正对 target
camera.lookAt(target);
// 6) 同步 OrbitControls 的内部状态一次,之后不要在本函数里重复调用
controls.update();
}