|
@@ -11,9 +11,12 @@ export class PltHandler extends BaseDataHandler {
|
|
|
this.parse = this.parse.bind(this); // 确保方法绑定
|
|
|
this.createZoneMesh = this.createZoneMesh.bind(this);
|
|
|
this.originalData = null;
|
|
|
- this.extremeMarkers = new THREE.Group();
|
|
|
- this.extremeMarkers.name = 'ExtremeMarkers';
|
|
|
- this.scene.add(this.extremeMarkers);
|
|
|
+ this.contourMaterial = new THREE.LineBasicMaterial({
|
|
|
+ color: 0xff0000, // 红色,便于调试曲面效果
|
|
|
+ linewidth: 2,
|
|
|
+ linecap: 'round',
|
|
|
+ linejoin: 'round'
|
|
|
+ });
|
|
|
}
|
|
|
parse(data, options = {}) {
|
|
|
if (!data?.zones) return;
|
|
@@ -416,4 +419,324 @@ export class PltHandler extends BaseDataHandler {
|
|
|
visible: mesh.visible
|
|
|
}));
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 为所有域生成等值线
|
|
|
+ * @param {string} variableName 变量名
|
|
|
+ * @param {Object} options 配置
|
|
|
+ * @returns {boolean} 是否全部成功
|
|
|
+ */
|
|
|
+generateContoursForAllZones(variableName, options = {}) {
|
|
|
+ console.group("批量生成等值线调试信息");
|
|
|
+ let allSuccess = true;
|
|
|
+
|
|
|
+ this.objectMap.forEach((mesh, zoneName) => {
|
|
|
+ if (mesh.visible) {
|
|
|
+ const success = this.generateContour(zoneName, variableName, options);
|
|
|
+ if (!success) {
|
|
|
+ console.warn(`域 ${zoneName} 的等值线生成失败`);
|
|
|
+ allSuccess = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log("批量生成等值线完成", { allSuccess });
|
|
|
+ console.groupEnd();
|
|
|
+ return allSuccess;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成等值线
|
|
|
+ * @param {string} zoneName 区域名称
|
|
|
+ * @param {string} variableName 变量名
|
|
|
+ * @param {Object} options 配置
|
|
|
+ */
|
|
|
+ generateContour(zoneName, variableName, options = {}) {
|
|
|
+ console.group("生成等值线调试信息");
|
|
|
+
|
|
|
+ const mesh = this.objectMap.get(zoneName);
|
|
|
+ if (!mesh) {
|
|
|
+ console.error("找不到指定区域的网格");
|
|
|
+ console.groupEnd();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const cacheKey = `${variableName}_${options.levels}_${options.minValue}_${options.maxValue}`;
|
|
|
+ if (mesh.userData.contourCache?.[cacheKey]) {
|
|
|
+ console.log(`使用缓存的等值线: ${zoneName}, ${variableName}`);
|
|
|
+ this.scene.add(mesh.userData.contourCache[cacheKey].group);
|
|
|
+ mesh.visible = false;
|
|
|
+ console.groupEnd();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ const zoneData = mesh.userData.originalZone;
|
|
|
+ if (!zoneData?.variables?.[variableName]) {
|
|
|
+ console.error(`变量不存在`, {
|
|
|
+ zone: zoneName,
|
|
|
+ requestedVar: variableName,
|
|
|
+ availableVars: Object.keys(zoneData.variables || {})
|
|
|
+ });
|
|
|
+ console.groupEnd();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const positions = mesh.geometry.attributes.position.array;
|
|
|
+ const scalarData = zoneData.variables[variableName];
|
|
|
+
|
|
|
+ if (positions.length / 3 !== scalarData.length) {
|
|
|
+ console.error("顶点数与标量数据长度不匹配", {
|
|
|
+ positionsLength: positions.length / 3,
|
|
|
+ scalarDataLength: scalarData.length
|
|
|
+ });
|
|
|
+ console.groupEnd();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 数据清洗
|
|
|
+ const validIndices = [];
|
|
|
+ for (let i = 0; i < scalarData.length; i++) {
|
|
|
+ const x = positions[i * 3];
|
|
|
+ const y = positions[i * 3 + 1];
|
|
|
+ const z = positions[i * 3 + 2];
|
|
|
+ if (
|
|
|
+ !isNaN(scalarData[i]) && isFinite(scalarData[i]) &&
|
|
|
+ !isNaN(x) && !isNaN(y) && !isNaN(z) &&
|
|
|
+ isFinite(x) && isFinite(y) && isFinite(z)
|
|
|
+ ) {
|
|
|
+ validIndices.push(i);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (validIndices.length === 0) {
|
|
|
+ console.error("无有效数据点");
|
|
|
+ console.groupEnd();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建清洁几何体
|
|
|
+ const cleanGeometry = new THREE.BufferGeometry();
|
|
|
+ const cleanPositions = new Float32Array(validIndices.length * 3);
|
|
|
+ const cleanScalars = new Float32Array(validIndices.length);
|
|
|
+ const cleanIndices = [];
|
|
|
+
|
|
|
+ const indexMap = new Map();
|
|
|
+ validIndices.forEach((origIdx, newIdx) => {
|
|
|
+ indexMap.set(origIdx, newIdx);
|
|
|
+ cleanPositions[newIdx * 3] = positions[origIdx * 3];
|
|
|
+ cleanPositions[newIdx * 3 + 1] = positions[origIdx * 3 + 1];
|
|
|
+ cleanPositions[newIdx * 3 + 2] = positions[origIdx * 3 + 2];
|
|
|
+ cleanScalars[newIdx] = scalarData[origIdx];
|
|
|
+ });
|
|
|
+
|
|
|
+ const originalIndices = mesh.geometry.index?.array || [];
|
|
|
+ for (let i = 0; i < originalIndices.length; i += 3) {
|
|
|
+ const i0 = indexMap.get(originalIndices[i]);
|
|
|
+ const i1 = indexMap.get(originalIndices[i + 1]);
|
|
|
+ const i2 = indexMap.get(originalIndices[i + 2]);
|
|
|
+ if (i0 !== undefined && i1 !== undefined && i2 !== undefined) {
|
|
|
+ cleanIndices.push(i0, i1, i2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ cleanGeometry.setAttribute('position', new THREE.BufferAttribute(cleanPositions, 3));
|
|
|
+ cleanGeometry.setIndex(new THREE.BufferAttribute(new Uint32Array(cleanIndices), 1));
|
|
|
+ cleanGeometry.computeVertexNormals();
|
|
|
+ cleanGeometry.computeBoundingSphere();
|
|
|
+
|
|
|
+ if (isNaN(cleanGeometry.boundingSphere.radius)) {
|
|
|
+ console.error("边界球计算失败,使用默认值");
|
|
|
+ cleanGeometry.boundingSphere = new THREE.Sphere(new THREE.Vector3(0, 0, 0), 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 生成等值线
|
|
|
+ const contours = this.calculateContours(cleanGeometry, cleanScalars, {
|
|
|
+ levels: options.levels || 5,
|
|
|
+ minValue: options.minValue ?? Math.min(...cleanScalars),
|
|
|
+ maxValue: options.maxValue ?? Math.max(...cleanScalars)
|
|
|
+ });
|
|
|
+
|
|
|
+ if (contours.length === 0) {
|
|
|
+ console.error("未生成有效等值线");
|
|
|
+ console.groupEnd();
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建等值线组
|
|
|
+ const contourGroup = new THREE.Group();
|
|
|
+ contourGroup.name = `${zoneName}_contour`;
|
|
|
+
|
|
|
+ contours.forEach(points => {
|
|
|
+ const lineGeometry = new THREE.BufferGeometry().setFromPoints(points);
|
|
|
+ contourGroup.add(new THREE.Line(lineGeometry, this.contourMaterial));
|
|
|
+ });
|
|
|
+
|
|
|
+ mesh.visible = false;
|
|
|
+ this.removeContour(zoneName);
|
|
|
+ mesh.userData.contourGroup = contourGroup;
|
|
|
+ this.scene.add(contourGroup);
|
|
|
+
|
|
|
+ mesh.userData.contourCache = mesh.userData.contourCache || {};
|
|
|
+ mesh.userData.contourCache[cacheKey] = { group: contourGroup };
|
|
|
+
|
|
|
+ console.log("等值线生成成功", {
|
|
|
+ points: cleanPositions.length / 3,
|
|
|
+ contours: contours.length
|
|
|
+ });
|
|
|
+ console.groupEnd();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ createCleanGeometry(originalGeometry, scalarData) {
|
|
|
+ const positions = originalGeometry.attributes.position.array;
|
|
|
+ const validIndices = [];
|
|
|
+
|
|
|
+ // 严格验证每个顶点
|
|
|
+ for (let i = 0; i < scalarData.length; i++) {
|
|
|
+ const x = positions[i * 3];
|
|
|
+ const y = positions[i * 3 + 1];
|
|
|
+ const z = positions[i * 3 + 2];
|
|
|
+
|
|
|
+ if (!isNaN(scalarData[i]) &&
|
|
|
+ !isNaN(x) && !isNaN(y) && !isNaN(z) &&
|
|
|
+ isFinite(x) && isFinite(y) && isFinite(z)) {
|
|
|
+ validIndices.push(i);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建新几何体
|
|
|
+ const geometry = new THREE.BufferGeometry();
|
|
|
+ const newPositions = new Float32Array(validIndices.length * 3);
|
|
|
+
|
|
|
+ validIndices.forEach((origIdx, newIdx) => {
|
|
|
+ newPositions[newIdx * 3] = positions[origIdx * 3];
|
|
|
+ newPositions[newIdx * 3 + 1] = positions[origIdx * 3 + 1];
|
|
|
+ newPositions[newIdx * 3 + 2] = positions[origIdx * 3 + 2];
|
|
|
+ });
|
|
|
+
|
|
|
+ geometry.setAttribute('position', new THREE.BufferAttribute(newPositions, 3));
|
|
|
+ geometry.computeVertexNormals();
|
|
|
+
|
|
|
+ // 强制计算边界球
|
|
|
+ geometry.computeBoundingSphere();
|
|
|
+ if (isNaN(geometry.boundingSphere.radius)) {
|
|
|
+ console.error("边界球计算失败,手动设置默认值");
|
|
|
+ geometry.boundingSphere = new THREE.Sphere(
|
|
|
+ new THREE.Vector3(0, 0, 0),
|
|
|
+ Math.sqrt(newPositions.length)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return geometry;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算等值线
|
|
|
+ */
|
|
|
+ calculateContours(geometry, scalarData, options) {
|
|
|
+ console.group("计算等值线调试信息");
|
|
|
+ const positions = geometry.attributes.position.array;
|
|
|
+ const indices = geometry.index?.array || [];
|
|
|
+ const { levels, minValue, maxValue } = options;
|
|
|
+ const contours = [];
|
|
|
+
|
|
|
+ try {
|
|
|
+ const step = (maxValue - minValue) / (levels || 1); // 防止除零
|
|
|
+
|
|
|
+ // 遍历三角面
|
|
|
+ for (let i = 0; i < indices.length; i += 3) {
|
|
|
+ const i0 = indices[i];
|
|
|
+ const i1 = indices[i + 1];
|
|
|
+ const i2 = indices[i + 2];
|
|
|
+
|
|
|
+ const v0 = new THREE.Vector3(positions[i0 * 3], positions[i0 * 3 + 1], positions[i0 * 3 + 2]);
|
|
|
+ const v1 = new THREE.Vector3(positions[i1 * 3], positions[i1 * 3 + 1], positions[i1 * 3 + 2]);
|
|
|
+ const v2 = new THREE.Vector3(positions[i2 * 3], positions[i2 * 3 + 1], positions[i2 * 3 + 2]);
|
|
|
+ const s0 = scalarData[i0];
|
|
|
+ const s1 = scalarData[i1];
|
|
|
+ const s2 = scalarData[i2];
|
|
|
+
|
|
|
+ if (isNaN(s0) || isNaN(s1) || isNaN(s2) || !isFinite(s0) || !isFinite(s1) || !isFinite(s2)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (let level = 0; level <= levels; level++) {
|
|
|
+ const threshold = minValue + level * step;
|
|
|
+ const points = this.calculateTriangleContour(v0, v1, v2, s0, s1, s2, threshold);
|
|
|
+ if (points.length >= 2) { // 确保至少有 2 个点形成线段
|
|
|
+ contours.push(points);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log("总等值线数量", contours.length);
|
|
|
+ } catch (error) {
|
|
|
+ console.error("等值线计算失败", error);
|
|
|
+ } finally {
|
|
|
+ console.groupEnd();
|
|
|
+ }
|
|
|
+
|
|
|
+ return contours;
|
|
|
+ }
|
|
|
+
|
|
|
+ calculateTriangleContour(v0, v1, v2, s0, s1, s2, threshold) {
|
|
|
+ const points = [];
|
|
|
+
|
|
|
+ // 验证标量值范围
|
|
|
+ const minScalar = Math.min(s0, s1, s2);
|
|
|
+ const maxScalar = Math.max(s0, s1, s2);
|
|
|
+ if (threshold < minScalar || threshold > maxScalar || !isFinite(threshold)) {
|
|
|
+ return points; // 阈值超出范围或无效
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查三角形是否退化
|
|
|
+ const v01 = v1.clone().sub(v0).length();
|
|
|
+ const v12 = v2.clone().sub(v1).length();
|
|
|
+ const v20 = v0.clone().sub(v2).length();
|
|
|
+ if (v01 < 1e-6 || v12 < 1e-6 || v20 < 1e-6) {
|
|
|
+ return points; // 跳过退化三角形
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算交点
|
|
|
+ const edges = [
|
|
|
+ { vStart: v0, vEnd: v1, sStart: s0, sEnd: s1 }, // 边 0-1
|
|
|
+ { vStart: v1, vEnd: v2, sStart: s1, sEnd: s2 }, // 边 1-2
|
|
|
+ { vStart: v2, vEnd: v0, sStart: s2, sEnd: s0 } // 边 2-0
|
|
|
+ ];
|
|
|
+
|
|
|
+ const edgePoints = [];
|
|
|
+ edges.forEach(({ vStart, vEnd, sStart, sEnd }) => {
|
|
|
+ if ((sStart <= threshold && sEnd >= threshold) || (sStart >= threshold && sEnd <= threshold)) {
|
|
|
+ const delta = sEnd - sStart;
|
|
|
+ if (Math.abs(delta) < 1e-6) return; // 防止除零
|
|
|
+ const t = (threshold - sStart) / delta;
|
|
|
+ if (t >= 0 && t <= 1) { // 确保插值参数有效
|
|
|
+ const point = vStart.clone().lerp(vEnd, t);
|
|
|
+ edgePoints.push(point);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 每条等值线应有 2 个点
|
|
|
+ if (edgePoints.length === 2) {
|
|
|
+ points.push(...edgePoints);
|
|
|
+ }
|
|
|
+
|
|
|
+ return points;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 移除等值线
|
|
|
+ */
|
|
|
+ removeContour(zoneName) {
|
|
|
+ const mesh = this.objectMap.get(zoneName);
|
|
|
+ if (mesh?.userData?.contourGroup) {
|
|
|
+ this.scene.remove(mesh.userData.contourGroup);
|
|
|
+ mesh.userData.contourGroup.traverse(child => {
|
|
|
+ if (child.isLine) child.geometry.dispose();
|
|
|
+ });
|
|
|
+ delete mesh.userData.contourGroup;
|
|
|
+ delete mesh.userData.contourCache;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|