|
|
|
@ -8,6 +8,8 @@ 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 ()=>{
|
|
|
|
@ -121,7 +123,7 @@ function initScene() {
|
|
|
|
|
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 = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
|
|
|
|
|
renderer.setSize(C.clientWidth, C.clientHeight);
|
|
|
|
|
C.appendChild(renderer.domElement);
|
|
|
|
|
|
|
|
|
@ -248,6 +250,7 @@ function initScene() {
|
|
|
|
|
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) => {
|
|
|
|
@ -257,6 +260,7 @@ function initScene() {
|
|
|
|
|
});
|
|
|
|
|
geo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
|
|
|
|
|
|
|
|
|
|
// 2) 绘制所有点
|
|
|
|
|
const mat = new THREE.PointsMaterial({
|
|
|
|
|
color: 'blue',
|
|
|
|
|
size: 20,
|
|
|
|
@ -264,9 +268,31 @@ function initScene() {
|
|
|
|
|
depthWrite: false,
|
|
|
|
|
depthTest: false
|
|
|
|
|
});
|
|
|
|
|
const perfs = new THREE.Points(geo, mat);
|
|
|
|
|
perfs.renderOrder = 212; // 最后画
|
|
|
|
|
scene.add(perfs);
|
|
|
|
|
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();
|
|
|
|
@ -297,7 +323,6 @@ function initScene() {
|
|
|
|
|
controls.update();
|
|
|
|
|
}
|
|
|
|
|
zoomByScroll(8, 0.99);
|
|
|
|
|
// drawAxes();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function drawAxes() {
|
|
|
|
@ -473,6 +498,7 @@ function applyView() {
|
|
|
|
|
controls.target.copy(center);
|
|
|
|
|
camera.lookAt(center);
|
|
|
|
|
controls.update();
|
|
|
|
|
console.log("切换视图:" + sel);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initControls() {
|
|
|
|
@ -495,7 +521,11 @@ function initControls() {
|
|
|
|
|
if (et > timeMax) et = timeMax;
|
|
|
|
|
endTime.value = et;
|
|
|
|
|
});
|
|
|
|
|
document.getElementById('viewSelect').onchange = applyView;
|
|
|
|
|
viewSelect.addEventListener('change', () => {
|
|
|
|
|
// 确保 currentGroup 有值
|
|
|
|
|
currentGroup = currentGroup || dataGroup;
|
|
|
|
|
applyView();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getStartEndInput() {
|
|
|
|
@ -520,101 +550,39 @@ function updateTimeLabel(timeValue) {
|
|
|
|
|
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();
|
|
|
|
|
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();
|
|
|
|
|
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;
|
|
|
|
@ -625,6 +593,7 @@ async function startPlay() {
|
|
|
|
|
|
|
|
|
|
while (isPlaying && current <= end) {
|
|
|
|
|
current += interval;
|
|
|
|
|
// 4) 对每个文件,拉数据并生成一组等值面,添加到 dataGroup
|
|
|
|
|
await renderFrame(start, current);
|
|
|
|
|
document.getElementById('timeLabel').innerText = current;
|
|
|
|
|
}
|
|
|
|
@ -638,32 +607,52 @@ function stopPlay() {
|
|
|
|
|
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();
|
|
|
|
|
async function savePng() {
|
|
|
|
|
// —— 用 WebGL + CSS2D canvas 合成一张图
|
|
|
|
|
renderer.render(scene, camera);
|
|
|
|
|
labelRenderer.render(scene, camera);
|
|
|
|
|
|
|
|
|
|
while (isPlaying && current <= end) {
|
|
|
|
|
current += interval;
|
|
|
|
|
await renderFrame(start, current);
|
|
|
|
|
// 拿 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 shot = await html2canvas(container, { backgroundColor: null, useCORS: true });
|
|
|
|
|
const png = shot.toDataURL();
|
|
|
|
|
await fetch(interfaceUrl + '/save_frame', {
|
|
|
|
|
// 合并
|
|
|
|
|
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 })
|
|
|
|
|
});
|
|
|
|
|
document.getElementById('timeLabel').innerText = current;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function downloadGIF() {
|
|
|
|
|
// 合成 GIF 并下载
|
|
|
|
|
const res = await fetch(interfaceUrl + '/make_gif');
|
|
|
|
|
const blob = await res.blob();
|
|
|
|
@ -672,26 +661,58 @@ async function startRecording() {
|
|
|
|
|
a.href = url; a.download = `animation_${Date.now()}.gif`;
|
|
|
|
|
document.body.appendChild(a); a.click();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stopPlay();
|
|
|
|
|
// 录制:逻辑跟播放一样,每帧多存图
|
|
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let initialFocused = false;
|
|
|
|
|
|
|
|
|
|
// —— 渲染单帧到 dataGroup(保持不变,只做数据 & Three.js 渲染)——
|
|
|
|
|
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;
|
|
|
|
|
// 前端时间输入是分钟,需要转换成秒再传给后端
|
|
|
|
|
// 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(); // 保证读到最新输入
|
|
|
|
@ -702,97 +723,65 @@ async function renderFrame(startTime, endTime) {
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
const selected = checkFileStatus();
|
|
|
|
|
let volumeTotal = 0;
|
|
|
|
|
|
|
|
|
|
document.getElementById('volume').innerText = '体积:' + volume + '立方米';
|
|
|
|
|
for (let i = 0; i < selected.length; i++) {
|
|
|
|
|
url.searchParams.set('file', selected[i]);
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
// 3) 拿数据并渲染到 dataGroup
|
|
|
|
|
const res = await fetch(url);
|
|
|
|
|
const { meshes, bounds, volume } = await res.json();
|
|
|
|
|
volumeTotal += volume;
|
|
|
|
|
document.getElementById('volume').innerText = '体积:' + volumeTotal + '立方米';
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
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(
|
|
|
|
|
target.x + radius * sinP * cosA,
|
|
|
|
|
target.y + radius * cosP,
|
|
|
|
|
target.z + radius * sinP * sinA
|
|
|
|
|
ctr.x, ctr.y,
|
|
|
|
|
ctr.z + box.getSize(new THREE.Vector3()).length()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// 5) 让相机正对 target
|
|
|
|
|
camera.lookAt(target);
|
|
|
|
|
|
|
|
|
|
// 6) 同步 OrbitControls 的内部状态一次,之后不要在本函数里重复调用
|
|
|
|
|
controls.update();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
startClick = false;
|
|
|
|
|
}
|
|
|
|
|