Jelajahi Sumber

等值线功能开发

lichunyang 2 bulan lalu
induk
melakukan
be3cb58e48

+ 18 - 11
src/components/ThreeScene/ThreeScene.vue

@@ -67,17 +67,21 @@ const {
   hideAll,
   getZoneVisibility,
   setZoneVisibility,
-  updateColorMapping
+  updateColorMapping,
+  generateContour,
+  generateContoursForAllZones,
+  removeContour
 } = useThree(canvasRef, props)
 
 onMounted(() => {
-  nextTick(() => { // 等待DOM更新
-    init();
+  nextTick(() => {
+    // 等待DOM更新
+    init()
     // 首次手动触发resize
-    handleResize(); 
-    window.addEventListener('resize', handleResize);
-  });
-});
+    handleResize()
+    window.addEventListener("resize", handleResize)
+  })
+})
 
 const handleResize = () => {
   threeResize() // 调用useThree提供的统一方法
@@ -88,8 +92,8 @@ onUnmounted(() => {
   window.removeEventListener("resize", handleResize)
 })
 
-defineExpose({ 
-  loadFile, 
+defineExpose({
+  loadFile,
   updateData,
   showZone,
   hideZone,
@@ -98,8 +102,11 @@ defineExpose({
   hideAll,
   getZoneVisibility,
   setZoneVisibility,
-  updateColorMapping
-   })
+  updateColorMapping,
+  generateContour,
+  generateContoursForAllZones,
+  removeContour
+})
 </script>
 
 <style>

+ 45 - 86
src/components/cloudChart/dialog/ContourDialog.vue

@@ -1,4 +1,5 @@
 <template>
+  <!-- 保持原有template部分完全不变 -->
   <SubDialog
     :modelValue="modelValue"
     @update:modelValue="$emit('update:modelValue', $event)"
@@ -25,7 +26,7 @@
             <el-form-item label="标量名:" :label-width="formLabelWidth1">
               <el-select v-model="contourValue.scalarname2" style="width: 100%">
                 <el-option
-                  v-for="item in scalarname2" 
+                  v-for="item in scalarname2options" 
                   :key="item.value" 
                   :label="item.label" 
                   :value="item.value"
@@ -41,7 +42,11 @@
           </template>
           <el-form label-position="left">
             <el-form-item label="层级:" :label-width="formLabelWidth1">
-              <el-input v-model="contourValue.cengji"></el-input>
+              <el-input-number 
+                v-model="contourValue.cengji" 
+                :min="1" 
+                :max="50"
+              />
             </el-form-item>
           </el-form>
         </el-collapse-item>
@@ -52,10 +57,16 @@
           </template>
           <el-form label-position="left">
             <el-form-item label="最大值:" :label-width="formLabelWidth1">
-              <el-input v-model="contourValue.max"></el-input>
+              <el-input-number 
+                v-model="contourValue.max" 
+                :step="0.01"
+              />
             </el-form-item>
             <el-form-item label="最小值:" :label-width="formLabelWidth1">
-              <el-input v-model="contourValue.min"></el-input>
+              <el-input-number 
+                v-model="contourValue.min" 
+                :step="0.01"
+              />
             </el-form-item>
           </el-form>
         </el-collapse-item>
@@ -65,109 +76,63 @@
 </template>
 
 <script setup>
-import { ref, defineProps, defineEmits } from 'vue'
-import SubDialog from './SubDialog.vue'
+import { ref, watch, computed } from 'vue'
 
 const props = defineProps({
   modelValue: Boolean,
-  activeZone: String,
-  variableOptions: {
-    type: Array,
-    default: () => []
-  },
   initialData: {
     type: Object,
-    default: () => ({ ranges: {} })
+    default: () => ({
+      ranges: {},
+      currentZone: '',
+      variables: []
+    })
   }
-});
+})
 
 const emit = defineEmits(['update:modelValue', 'confirm', 'cancel'])
 
-// 表单标签宽度
-let formLabelWidth1 = ref(140)
-
-// 折叠面板当前激活项
+const formLabelWidth1 = ref(140)
 const activeNames2 = ref(['1', '2', '3'])
 
-// 表单数据
 const contourValue = ref({
-  name: 'contour_1',
-  type: 'uniform',
-  scalarname2: props.variableOptions[0]?.value || '',
+  name: 'contour',
+  type: 'auto',
+  scalarname2: '',
   cengji: 10,
-  max: props.initialData?.ranges[props.variableOptions[0]?.value]?.max.toFixed(2) || '1.00',
-  min: props.initialData?.ranges[props.variableOptions[0]?.value]?.min.toFixed(2) || '0.00',
-  width: 0.02,
-  color: '#000000',
-  autoRange: true
-});
-
-const scalarname2 = ref([])
-
-const availableVariables = computed(() => {
-  return props.variableOptions.map(item => ({
-    label: item,
-    value: item
-  }))
+  max: 0,
+  min: 0
 })
 
-const contourSpacing = computed(() => {
-  const levelCount = parseInt(contourValue.value.cengji) || 10
-  return (parseFloat(contourValue.value.max) - parseFloat(contourValue.value.min)) / levelCount
+const scalarname2options = computed(() => {
+  return props.initialData.variables?.map(varName => ({
+    label: varName,
+    value: varName
+  })) || []
 })
 
-// 初始化变量选项
-// 自动更新范围
+// 自动更新范围值
 watch(() => contourValue.value.scalarname2, (newVar) => {
-  if (contourValue.value.autoRange && props.initialData.ranges?.[newVar]) {
-    contourValue.value.max = props.initialData.ranges[newVar].max.toFixed(2)
-    contourValue.value.min = props.initialData.ranges[newVar].min.toFixed(2)
+  if (newVar && props.initialData.ranges[newVar]) {
+    contourValue.value.max = props.initialData.ranges[newVar].max
+    contourValue.value.min = props.initialData.ranges[newVar].min
   }
 })
 
-// 自动计算范围
-const updateAutoRange = () => {
-  if (!contourValue.value.autoRange || !props.initialData?.ranges) return
-  const variable = contourValue.value.scalarname2
-  if (variable && props.initialData.ranges[variable]) {
-    contourValue.value.max = props.initialData.ranges[variable].max.toFixed(2)
-    contourValue.value.min = props.initialData.ranges[variable].min.toFixed(2)
-  }
-}
-
-// 计算等值线间距
-const spacing = computed(() => {
-  return (parseFloat(contourValue.value.max) - parseFloat(contourValue.value.min)) / contourValue.value.cengji;
-});
-
-// 自动更新范围
-watch(() => contourValue.value.scalarname2, (newVar) => {
-  if (contourValue.value.autoRange && props.initialData?.ranges[newVar]) {
-    contourValue.value.max = props.initialData.ranges[newVar].max.toFixed(2);
-    contourValue.value.min = props.initialData.ranges[newVar].min.toFixed(2);
-  }
-});
-
-// 确认按钮处理
 const handleConfirm = () => {
-  const params = {
-    zoneName: props.activeZone,
+  emit('confirm', {
+    zoneName: props.initialData.currentZone,
     variableName: contourValue.value.scalarname2,
     options: {
-      spacing: spacing.value,
-      width: contourValue.value.width,
-      color: parseInt(contourValue.value.color.replace('#', '0x')),
+      levels: parseInt(contourValue.value.cengji),
       minValue: parseFloat(contourValue.value.min),
       maxValue: parseFloat(contourValue.value.max),
-      opacity: 1.0
+      name: contourValue.value.name
     }
-  };
-  
-  emit('confirm', params);
-  emit('update:modelValue', false);
-};
+  })
+  emit('update:modelValue', false)
+}
 
-// 取消操作
 const handleCancel = () => {
   emit('cancel')
   emit('update:modelValue', false)
@@ -175,28 +140,22 @@ const handleCancel = () => {
 </script>
 
 <style scoped>
+/* 保持原有样式不变 */
 .contour-dialog-content {
   padding: 0 10px;
   overflow-y: auto;
   height: 100%;
 }
-
-/* 折叠面板标题样式 */
 .collapse-title {
   font-weight: bold;
   font-size: 14px;
 }
-
-/* 调整折叠面板内容间距 */
 :deep(.el-collapse-item__content) {
   padding-bottom: 10px;
 }
-
 :deep(.el-dialog__body) {
   padding: 10px !important;
 }
-
-/* 调整表单项间距 */
 .el-form-item {
   margin-bottom: 8px;
 }

+ 66 - 28
src/components/cloudChart/index.vue

@@ -97,7 +97,7 @@
         :plt-data="pltData"
         :active-zone="pltStore.currentZone"
         :variable-options="pltData?.metadata?.variables || []"
-        :initial-data="{ ranges: calculateVariableRanges() }"
+        :initial-data="contourInitialData"
         @confirm="handleContourConfirm"
         @cancel="contourDialogVisible = false"
       />
@@ -120,21 +120,6 @@
             style="width: 100%; height: 100%"
           />
         </div>
-        <!-- 色卡 -->
-        <div
-          class="color-bar-container"
-          v-if="colorBarConfig && colorBarConfig.visible"
-        >
-          <div class="color-bar" :style="colorBarStyle"></div>
-          <div class="color-bar-labels">
-            <span v-for="(label, index) in colorBarLabels" :key="index">{{
-              label
-            }}</span>
-          </div>
-          <div class="color-bar-title" v-if="colorBarConfig.showTitle">
-            {{ colorBarConfig.title || "Color Bar" }}
-          </div>
-        </div>
       </div>
     </div>
   </el-dialog>
@@ -244,6 +229,12 @@ let pltData = ref()
 const renderer = ref()
 const pltStore = usePltDataStore()
 
+const contourInitialData = ref({
+  ranges: {},
+  currentZone: '',
+  variables: []
+});
+
 // 处理按钮点击
 const handleButtonClick = (btnName) => {
   switch (btnName) {
@@ -260,7 +251,7 @@ const handleButtonClick = (btnName) => {
       colorCardDialogVisible.value = true
       break
     case "等值线":
-      contourDialogVisible.value = true
+      openContourDialog();
       break
     default:
       break
@@ -305,18 +296,53 @@ const handleColorCardConfirm = (data) => {
 }
 
 const handleContourConfirm = (params) => {
-  threeSceneRef.value?.enableContour(
-    params.zoneName,
-    params.variableName,
-    params.options
-  )
+  console.log("原始参数:", JSON.parse(JSON.stringify(params)));
+
+  // 参数标准化处理
+  const processedParams = {
+    zoneName: params.zoneName,
+    variableName: params.variableName,
+    options: {
+      levels: Math.max(1, parseInt(params.options?.levels) || 10),
+      minValue: parseFloat(params.options?.minValue) || 0,
+      maxValue: parseFloat(params.options?.maxValue) || 1
+    }
+  };
+
+  console.log("处理后的参数:", processedParams);
+
+  threeSceneRef.value?.generateContoursForAllZones(
+    // processedParams.zoneName,
+    processedParams.variableName,
+    processedParams.options
+  );
+};
+
+const openContourDialog = () => {
+  contourDialogVisible.value = true;
   
-  threeSceneRef.value?.updateColorMapping(params.variableName, {
-    min: params.options.minValue,
-    max: params.options.maxValue
-  })
-  contourDialogVisible.value = false
-}
+  // 获取当前激活的区域(假设默认显示第一个可见区域)
+  const visibleZones = pltData.value?.zones?.filter(zone => 
+    threeSceneRef.value?.getZoneVisibility(zone.name)
+  );
+  const currentZone = visibleZones?.[0]?.name || '';
+
+  // 计算变量范围
+  const ranges = pltData.value?.zones?.reduce((acc, zone) => {
+    Object.entries(zone.variables || {}).forEach(([name, data]) => {
+      if (!acc[name]) acc[name] = { min: Infinity, max: -Infinity };
+      acc[name].min = Math.min(acc[name].min, ...data);
+      acc[name].max = Math.max(acc[name].max, ...data);
+    });
+    return acc;
+  }, {});
+
+  contourInitialData.value = {
+    ranges,
+    currentZone,  // 使用实际获取的区域名
+    variables: pltData.value?.metadata?.variables || []
+  };
+};
 
 const getImgPath = (url) => {
   return new URL(`../../assets/img/${url}`, import.meta.url).href
@@ -359,6 +385,18 @@ const getPltData = async (fpid) => {
     pltData.value = parsedData
     console.log("解析后的数据:", parsedData)
 
+    // 数据验证
+    parsedData.zones.forEach(zone => {
+      console.log(`区域 ${zone.name} 数据验证:`, {
+        vertexCount: zone.vertices?.length / 3,
+        varStatus: Object.entries(zone.variables || {}).map(([name, data]) => ({
+          name,
+          count: data.length,
+          nanCount: data.filter(v => isNaN(v)).length
+        }))
+      });
+    });
+
     pltStore.initialize(parsedData.zones)
     await nextTick()
     ;["far", "symm"].forEach((name) => {

+ 27 - 16
src/composables/useThree.js

@@ -27,7 +27,7 @@ import { CSS2DRenderer, CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRe
 import { XyzHandler } from '@/utils/three/objects/fileHandlers/XyzHandler.js';
 import { PltHandler } from '@/utils/three/dataHandlers/PltHandler.js';
 
-let cube, gui, animationId, helpers, lights;
+let gui, animationId, helpers, lights;
 export function useThree(canvasRef, props) {
   const scene = ref(null);
   const camera = ref(null);
@@ -106,11 +106,8 @@ export function useThree(canvasRef, props) {
 
   const loadFile = async (file) => {
     const extension = file.name.split('.').pop().toLowerCase();
-    console.log('File extension:', extension);
-    console.log('Available handlers:', fileHandlers); // 查看已注册的处理器
 
     const handler = fileHandlers?.[extension];
-    console.log('ffffffhandler', handler);
 
     if (!handler) {
       console.error(`Unsupported file type: ${extension}`);
@@ -153,13 +150,6 @@ export function useThree(canvasRef, props) {
     }
   };
 
-
-  const updateCube = (params) => {
-    if (cubeRef.value) {
-      cubeRef.value.updateParams(params);
-    }
-  };
-
   const setupEventListeners = () => {
     const pltHandler = dataHandlers.value.plt;
     if (!pltHandler) return;
@@ -260,6 +250,25 @@ export function useThree(canvasRef, props) {
     return false;
   };
 
+  const generateContour = (zoneName, variableName, options) => {
+    if (dataHandlers.value.plt) {
+      return dataHandlers.value.plt.generateContour(zoneName, variableName, options);
+    }
+  };
+
+  const generateContoursForAllZones = (variableName, options) => {
+    if (dataHandlers.value.plt) {
+      return dataHandlers.value.plt.generateContoursForAllZones(variableName, options);
+    }
+  };
+
+  const removeContour = (zoneName) => {
+    if (dataHandlers.value.plt) {
+      return dataHandlers.value.plt.removeContour(zoneName);
+    }
+
+  };
+
 
   const animate = () => {
     requestAnimationFrame(animate);
@@ -284,9 +293,9 @@ export function useThree(canvasRef, props) {
     window.removeEventListener('resize', () =>
       handleResize(canvasRef, camera, renderer));
 
-    if (controls) {
-      controls.value.dispose();
-    }
+    // if (controls) {
+    //   controls.value.dispose();
+    // }
 
     if (gui) {
       gui.destroy();
@@ -323,10 +332,12 @@ export function useThree(canvasRef, props) {
     controls,
     helpers,
     handleResize,
-    updateCube,
     loadFile,
     updateData,
     ...zoneControls,
-    updateColorMapping
+    updateColorMapping,
+    generateContour,
+    removeContour,
+    generateContoursForAllZones
   };
 }

+ 72 - 0
src/utils/three/dataHandlers/MarchingSquares.js

@@ -0,0 +1,72 @@
+export class MarchingSquares {
+    static generateContours(grid, threshold) {
+        const { width, height, data, minX, maxX, minY, maxY } = grid;
+        const contours = [];
+
+        for (let y = 0; y < height - 1; y++) {
+            for (let x = 0; x < width - 1; x++) {
+                // 获取当前单元格四个角的值
+                const val0 = data[y * width + x];
+                const val1 = data[y * width + x + 1];
+                const val2 = data[(y + 1) * width + x];
+                const val3 = data[(y + 1) * width + x + 1];
+
+                // 验证值有效性
+                if (isNaN(val0) || isNaN(val1) || isNaN(val2) || isNaN(val3)) {
+                    continue; // 跳过无效单元格
+                }
+
+                // 计算交点
+                const points = [];
+                if ((val0 > threshold) !== (val1 > threshold) && isFinite(val0) && isFinite(val1)) {
+                    const t = (threshold - val0) / (val1 - val0);
+                    const px = x + t;
+                    const py = y;
+                    points.push({
+                        x: minX + (px / (width - 1)) * (maxX - minX),
+                        y: minY + (py / (height - 1)) * (maxY - minY)
+                    });
+                }
+                if ((val1 > threshold) !== (val3 > threshold) && isFinite(val1) && isFinite(val3)) {
+                    const t = (threshold - val1) / (val3 - val1);
+                    const px = x + 1;
+                    const py = y + t;
+                    points.push({
+                        x: minX + (px / (width - 1)) * (maxX - minX),
+                        y: minY + (py / (height - 1)) * (maxY - minY)
+                    });
+                }
+                if ((val3 > threshold) !== (val2 > threshold) && isFinite(val3) && isFinite(val2)) {
+                    const t = (threshold - val2) / (val3 - val2);
+                    const px = x + t;
+                    const py = y + 1;
+                    points.push({
+                        x: minX + (px / (width - 1)) * (maxX - minX),
+                        y: minY + (py / (height - 1)) * (maxY - minY)
+                    });
+                }
+                if ((val2 > threshold) !== (val0 > threshold) && isFinite(val2) && isFinite(val0)) {
+                    const t = (threshold - val0) / (val2 - val0);
+                    const px = x;
+                    const py = y + t;
+                    points.push({
+                        x: minX + (px / (width - 1)) * (maxX - minX),
+                        y: minY + (py / (height - 1)) * (maxY - minY)
+                    });
+                }
+
+                // 连接交点形成线段
+                if (points.length === 2) {
+                    contours.push(points);
+                } else if (points.length === 4) {
+                    // 简单处理双交点情况(可改进为更精确的拓扑分析)
+                    contours.push([points[0], points[1]]);
+                    contours.push([points[2], points[3]]);
+                }
+            }
+        }
+
+        console.log("生成等值线", { threshold, contourCount: contours.length });
+        return contours;
+    }
+}

+ 326 - 3
src/utils/three/dataHandlers/PltHandler.js

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