添加 MX-PD-盘古 项目文件
将 MX-PD-盘古 - new 目录下的所有文件添加到主仓库
This commit is contained in:
@@ -0,0 +1,718 @@
|
||||
using MainShell.Common.ViewModel;
|
||||
using MainShell.Common;
|
||||
using MainShell.Filewritable;
|
||||
using MainShell.Hardware;
|
||||
using MainShell.Models;
|
||||
using MainShell.Recipe.Models;
|
||||
using MainShell.Recipe.Models.SubstrateParameter;
|
||||
using MainShell.Recipe.View;
|
||||
using MaxwellFramework.Core.Common.Command;
|
||||
using Microsoft.Win32;
|
||||
using Stylet;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace MainShell.Recipe.ViewModel
|
||||
{
|
||||
public class MarkTeachViewModel : PropertyChangedBase
|
||||
{
|
||||
private const double PitchTolerance = 0.001d;
|
||||
|
||||
private readonly HardwareManager _hardwareManager;
|
||||
private readonly IWindowManager _windowManager;
|
||||
private SubstrateInfo _observedSubstrateInfo;
|
||||
private MarkCoordinatePreviewWindow _coordinatePreviewWindow;
|
||||
private MarkCoordinatePreviewWindowModel _coordinatePreviewWindowModel;
|
||||
|
||||
public MarkTeachViewModel(HardwareManager hardwareManager,IWindowManager windowManager)
|
||||
{
|
||||
_hardwareManager = hardwareManager;
|
||||
_windowManager = windowManager;
|
||||
AddMarkCmd = new DelegateCommand(AddMark);
|
||||
DeleteMarkCmd = new DelegateCommand(DeleteMark);
|
||||
TeachMarkCmd = new DelegateCommand(TeachMark);
|
||||
MoveToMarkCmd = new DelegateCommand(MoveToMark);
|
||||
MarkModelCmd = new DelegateCommand(MarkModel);
|
||||
VisionParSetCmd = new DelegateCommand(VisionParSet);
|
||||
GenerateCoordinatesCmd = new DelegateCommand(GenerateCoordinates);
|
||||
ImportCoordinatesCmd = new DelegateCommand(ImportCoordinates);
|
||||
ClearCoordinatesCmd = new DelegateCommand(ClearCoordinates);
|
||||
ShowCoordinatePreviewCmd = new DelegateCommand(ShowCoordinatePreview);
|
||||
RefreshTemplates();
|
||||
}
|
||||
|
||||
public ObservableCollection<string> AvailableTemplates { get; set; } = new ObservableCollection<string>();
|
||||
|
||||
private void RefreshTemplates()
|
||||
{
|
||||
AvailableTemplates.Clear();
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(Paths.TemplateBasePath))
|
||||
{
|
||||
var files = Directory.GetFiles(Paths.TemplateBasePath);
|
||||
foreach (var file in files)
|
||||
{
|
||||
AvailableTemplates.Add(Path.GetFileName(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Handle or log exception
|
||||
}
|
||||
}
|
||||
|
||||
private SubstrateRecipe _substrateRecipe;
|
||||
public SubstrateRecipe SubstrateRecipe
|
||||
{
|
||||
get => _substrateRecipe;
|
||||
set
|
||||
{
|
||||
if (SetAndNotify(ref _substrateRecipe, value))
|
||||
{
|
||||
EnsureCoordinateGenerationState();
|
||||
ObserveSubstrateInfo(value?.SubstrateInfo);
|
||||
NotifyOfPropertyChange(() => MarkPars);
|
||||
NotifyOfPropertyChange(() => MarkDatas);
|
||||
NotifyOfPropertyChange(() => CoordinateGenerationState);
|
||||
NotifyOfPropertyChange(() => CoordinatePreviewPoints);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SubtrateMarkPars MarkPars => SubstrateRecipe?.SubtrateMarkParameterInfo;
|
||||
public ObservableCollection<MarkData> MarkDatas => MarkPars?.MarkDatas;
|
||||
public MarkCoordinateGenerationState CoordinateGenerationState => MarkPars?.CoordinateGenerationState;
|
||||
public ObservableCollection<MarkCoordinatePoint> CoordinatePreviewPoints => CoordinateGenerationState?.Points;
|
||||
|
||||
private MarkData _selectedMarkData;
|
||||
public MarkData SelectedMarkData
|
||||
{
|
||||
get => _selectedMarkData;
|
||||
set => SetAndNotify(ref _selectedMarkData, value);
|
||||
}
|
||||
private bool _isTeachPose;
|
||||
|
||||
public bool IsTeachPose
|
||||
{
|
||||
get { return _isTeachPose; }
|
||||
set { SetAndNotify(ref _isTeachPose, value); }
|
||||
}
|
||||
|
||||
#region Commands
|
||||
|
||||
public ICommand AddMarkCmd { get; }
|
||||
public ICommand DeleteMarkCmd { get; }
|
||||
public ICommand TeachMarkCmd { get; }
|
||||
public ICommand MoveToMarkCmd { get; }
|
||||
public ICommand MarkModelCmd { get; }
|
||||
public ICommand VisionParSetCmd { get; }
|
||||
public ICommand GenerateCoordinatesCmd { get; }
|
||||
public ICommand ImportCoordinatesCmd { get; }
|
||||
public ICommand ClearCoordinatesCmd { get; }
|
||||
public ICommand ShowCoordinatePreviewCmd { get; }
|
||||
|
||||
private void ObserveSubstrateInfo(SubstrateInfo substrateInfo)
|
||||
{
|
||||
if (_observedSubstrateInfo != null)
|
||||
{
|
||||
_observedSubstrateInfo.PropertyChanged -= OnSubstrateInfoChanged;
|
||||
}
|
||||
|
||||
_observedSubstrateInfo = substrateInfo;
|
||||
|
||||
if (_observedSubstrateInfo != null)
|
||||
{
|
||||
_observedSubstrateInfo.PropertyChanged += OnSubstrateInfoChanged;
|
||||
}
|
||||
|
||||
SyncCoordinateGenerationSource();
|
||||
}
|
||||
|
||||
private void OnSubstrateInfoChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(e.PropertyName)
|
||||
|| e.PropertyName == nameof(SubstrateInfo.RowNumber)
|
||||
|| e.PropertyName == nameof(SubstrateInfo.ColNumber)
|
||||
|| e.PropertyName == nameof(SubstrateInfo.PitchX)
|
||||
|| e.PropertyName == nameof(SubstrateInfo.PitchY))
|
||||
{
|
||||
SyncCoordinateGenerationSource();
|
||||
}
|
||||
}
|
||||
|
||||
private bool EnsureCoordinateGenerationState()
|
||||
{
|
||||
if (SubstrateRecipe == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (SubstrateRecipe.SubtrateMarkParameterInfo == null)
|
||||
{
|
||||
SubstrateRecipe.SubtrateMarkParameterInfo = new SubtrateMarkPars();
|
||||
}
|
||||
|
||||
if (SubstrateRecipe.SubtrateMarkParameterInfo.CoordinateGenerationState == null)
|
||||
{
|
||||
SubstrateRecipe.SubtrateMarkParameterInfo.CoordinateGenerationState = new MarkCoordinateGenerationState();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SyncCoordinateGenerationSource()
|
||||
{
|
||||
if (!EnsureCoordinateGenerationState())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var substrateInfo = SubstrateRecipe?.SubstrateInfo;
|
||||
var coordinateState = CoordinateGenerationState;
|
||||
if (substrateInfo == null || coordinateState == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
coordinateState.Rows = substrateInfo.RowNumber;
|
||||
coordinateState.Cols = substrateInfo.ColNumber;
|
||||
coordinateState.PitchX = substrateInfo.PitchX;
|
||||
coordinateState.PitchY = substrateInfo.PitchY;
|
||||
|
||||
NotifyOfPropertyChange(() => CoordinateGenerationState);
|
||||
NotifyOfPropertyChange(() => CoordinatePreviewPoints);
|
||||
}
|
||||
|
||||
private bool EnsureRecipeSelected()
|
||||
{
|
||||
if (SubstrateRecipe != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
MwMessageBox.Show("请先选择或创建基板配方", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AddMark(object obj)
|
||||
{
|
||||
if (MarkDatas == null) return;
|
||||
var newMark = new MarkData
|
||||
{
|
||||
TemplateName = $"Mark{MarkDatas.Count + 1}",
|
||||
BasePos = new MPoint(),
|
||||
CameraPos = new MPoint()
|
||||
};
|
||||
MarkDatas.Add(newMark);
|
||||
SelectedMarkData = newMark;
|
||||
}
|
||||
|
||||
private void DeleteMark(object obj)
|
||||
{
|
||||
if (SelectedMarkData != null && MarkDatas != null)
|
||||
{
|
||||
MarkDatas.Remove(SelectedMarkData);
|
||||
}
|
||||
}
|
||||
|
||||
private void TeachMark(object obj)
|
||||
{
|
||||
if (SelectedMarkData == null) return;
|
||||
|
||||
var device = _hardwareManager.CameraAxisManager.TopCameraAxisDevices.FirstOrDefault();
|
||||
if (device != null)
|
||||
{
|
||||
if (SelectedMarkData.BasePos == null) SelectedMarkData.BasePos = new MPoint();
|
||||
if (SelectedMarkData.CameraPos == null) SelectedMarkData.CameraPos = new MPoint();
|
||||
|
||||
if (device.AxisX != null) SelectedMarkData.BasePos.X = device.AxisX.State.ActualPos;
|
||||
if (device.AxisY != null) SelectedMarkData.BasePos.Y = device.AxisY.State.ActualPos;
|
||||
|
||||
if (device.AxisX != null) SelectedMarkData.CameraPos.X = device.AxisX.State.ActualPos;
|
||||
if (device.AxisY != null) SelectedMarkData.CameraPos.Y = device.AxisY.State.ActualPos;
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveToMark(object obj)
|
||||
{
|
||||
if (SelectedMarkData == null) return;
|
||||
var device = _hardwareManager.CameraAxisManager.TopCameraAxisDevices.FirstOrDefault();
|
||||
if (device != null)
|
||||
{
|
||||
// TODO: Implement Safe Move logic using AxisMotion or similar
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkModel(object obj)
|
||||
{
|
||||
// TODO: Open Mark Model Dialog
|
||||
// Refresh templates after returning from model dialog as new templates might have been created
|
||||
RefreshTemplates();
|
||||
}
|
||||
|
||||
private void VisionParSet(object obj)
|
||||
{
|
||||
// TODO: Open Vision Parameter Settings
|
||||
CameraSettingsViewModel settingsViewModel=new CameraSettingsViewModel(_windowManager,_hardwareManager);
|
||||
settingsViewModel.Initialize(_substrateRecipe.SubtrateMarkParameterInfo.MarkVisionConfig,
|
||||
_substrateRecipe.SubtrateMarkParameterInfo.MarkLightConfig, Common.CameraType.TopPositionCamera);
|
||||
_windowManager.ShowDialog(settingsViewModel);
|
||||
}
|
||||
|
||||
private void GenerateCoordinates(object obj)
|
||||
{
|
||||
if (!EnsureRecipeSelected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SyncCoordinateGenerationSource();
|
||||
var coordinateState = CoordinateGenerationState;
|
||||
if (coordinateState == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (coordinateState.Rows <= 0 || coordinateState.Cols <= 0)
|
||||
{
|
||||
MwMessageBox.Show("基板信息中的行列数必须大于 0。", "生成失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (coordinateState.Cols > 1 && Math.Abs(coordinateState.PitchX) <= double.Epsilon)
|
||||
{
|
||||
MwMessageBox.Show("基板信息中的 Pitch X 无效,无法生成点位。", "生成失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (coordinateState.Rows > 1 && Math.Abs(coordinateState.PitchY) <= double.Epsilon)
|
||||
{
|
||||
MwMessageBox.Show("基板信息中的 Pitch Y 无效,无法生成点位。", "生成失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
var generatedPoints = new ObservableCollection<MarkCoordinatePoint>();
|
||||
var index = 1;
|
||||
for (var row = 1; row <= coordinateState.Rows; row++)
|
||||
{
|
||||
for (var col = 1; col <= coordinateState.Cols; col++)
|
||||
{
|
||||
generatedPoints.Add(new MarkCoordinatePoint
|
||||
{
|
||||
Index = index++,
|
||||
PointName = CreatePointName(row, col),
|
||||
Row = row,
|
||||
Col = col,
|
||||
TheoryX = (col - 1) * coordinateState.PitchX,
|
||||
TheoryY = (row - 1) * coordinateState.PitchY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
coordinateState.Points = generatedPoints;
|
||||
NotifyOfPropertyChange(() => CoordinatePreviewPoints);
|
||||
}
|
||||
|
||||
private void ImportCoordinates(object obj)
|
||||
{
|
||||
if (!EnsureRecipeSelected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new OpenFileDialog
|
||||
{
|
||||
Title = "导入坐标文件",
|
||||
Filter = "CSV Files (*.csv)|*.csv|All Files (*.*)|*.*",
|
||||
CheckFileExists = true,
|
||||
Multiselect = false,
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<MarkCoordinatePoint> importedPoints;
|
||||
string errorMessage;
|
||||
if (!TryReadCoordinateFile(dialog.FileName, out importedPoints, out errorMessage))
|
||||
{
|
||||
MwMessageBox.Show(errorMessage, "导入失败", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (importedPoints.Count == 0)
|
||||
{
|
||||
MwMessageBox.Show("导入文件中没有可用的坐标数据。", "导入失败", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
SyncCoordinateGenerationSource();
|
||||
var validationMessage = BuildImportValidationMessage(importedPoints);
|
||||
if (!string.IsNullOrWhiteSpace(validationMessage))
|
||||
{
|
||||
var confirmResult = MwMessageBox.Show(
|
||||
validationMessage + Environment.NewLine + Environment.NewLine + "是否继续导入?",
|
||||
"导入校验",
|
||||
MessageBoxButton.YesNo,
|
||||
MessageBoxImage.Warning,
|
||||
MessageBoxResult.No);
|
||||
|
||||
if (confirmResult != MessageBoxResult.Yes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
CoordinateGenerationState.Points = new ObservableCollection<MarkCoordinatePoint>(importedPoints);
|
||||
NotifyOfPropertyChange(() => CoordinatePreviewPoints);
|
||||
}
|
||||
|
||||
private void ClearCoordinates(object obj)
|
||||
{
|
||||
if (!EnsureRecipeSelected() || CoordinateGenerationState == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CoordinateGenerationState.Points.Clear();
|
||||
NotifyOfPropertyChange(() => CoordinatePreviewPoints);
|
||||
}
|
||||
|
||||
private void ShowCoordinatePreview(object obj)
|
||||
{
|
||||
if (!EnsureRecipeSelected())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_coordinatePreviewWindow != null)
|
||||
{
|
||||
_coordinatePreviewWindowModel?.RefreshViewState();
|
||||
|
||||
if (_coordinatePreviewWindow.WindowState == WindowState.Minimized)
|
||||
{
|
||||
_coordinatePreviewWindow.WindowState = WindowState.Normal;
|
||||
}
|
||||
|
||||
_coordinatePreviewWindow.Activate();
|
||||
_coordinatePreviewWindow.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
_coordinatePreviewWindowModel = new MarkCoordinatePreviewWindowModel(this);
|
||||
_coordinatePreviewWindow = new MarkCoordinatePreviewWindow
|
||||
{
|
||||
DataContext = _coordinatePreviewWindowModel
|
||||
};
|
||||
|
||||
var ownerWindow = Application.Current?.Windows
|
||||
.OfType<Window>()
|
||||
.FirstOrDefault(window => window.IsActive && window != _coordinatePreviewWindow);
|
||||
if (ownerWindow != null)
|
||||
{
|
||||
_coordinatePreviewWindow.Owner = ownerWindow;
|
||||
}
|
||||
|
||||
_coordinatePreviewWindow.Closed += OnCoordinatePreviewWindowClosed;
|
||||
_coordinatePreviewWindow.Show();
|
||||
_coordinatePreviewWindow.Activate();
|
||||
}
|
||||
|
||||
private void OnCoordinatePreviewWindowClosed(object sender, EventArgs e)
|
||||
{
|
||||
if (_coordinatePreviewWindow != null)
|
||||
{
|
||||
_coordinatePreviewWindow.Closed -= OnCoordinatePreviewWindowClosed;
|
||||
}
|
||||
|
||||
_coordinatePreviewWindow = null;
|
||||
_coordinatePreviewWindowModel = null;
|
||||
}
|
||||
|
||||
private string BuildImportValidationMessage(IReadOnlyList<MarkCoordinatePoint> importedPoints)
|
||||
{
|
||||
var coordinateState = CoordinateGenerationState;
|
||||
if (coordinateState == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var importedRows = importedPoints.Select(point => point.Row).Distinct().Count();
|
||||
var importedCols = importedPoints.Select(point => point.Col).Distinct().Count();
|
||||
var importedPitchX = CalculatePitch(importedPoints, true);
|
||||
var importedPitchY = CalculatePitch(importedPoints, false);
|
||||
|
||||
var mismatchMessages = new List<string>();
|
||||
if (coordinateState.Rows > 0 && importedRows != coordinateState.Rows)
|
||||
{
|
||||
mismatchMessages.Add($"行数不匹配:基板信息为 {coordinateState.Rows},导入数据为 {importedRows}。");
|
||||
}
|
||||
|
||||
if (coordinateState.Cols > 0 && importedCols != coordinateState.Cols)
|
||||
{
|
||||
mismatchMessages.Add($"列数不匹配:基板信息为 {coordinateState.Cols},导入数据为 {importedCols}。");
|
||||
}
|
||||
|
||||
if (coordinateState.Cols > 1 && importedPitchX.HasValue && Math.Abs(importedPitchX.Value - coordinateState.PitchX) > PitchTolerance)
|
||||
{
|
||||
mismatchMessages.Add($"Pitch X 不匹配:基板信息为 {coordinateState.PitchX:F4},导入数据为 {importedPitchX.Value:F4}。");
|
||||
}
|
||||
|
||||
if (coordinateState.Rows > 1 && importedPitchY.HasValue && Math.Abs(importedPitchY.Value - coordinateState.PitchY) > PitchTolerance)
|
||||
{
|
||||
mismatchMessages.Add($"Pitch Y 不匹配:基板信息为 {coordinateState.PitchY:F4},导入数据为 {importedPitchY.Value:F4}。");
|
||||
}
|
||||
|
||||
return mismatchMessages.Count == 0 ? string.Empty : string.Join(Environment.NewLine, mismatchMessages);
|
||||
}
|
||||
|
||||
private static double? CalculatePitch(IReadOnlyList<MarkCoordinatePoint> points, bool isXAxis)
|
||||
{
|
||||
var pitchSamples = new List<double>();
|
||||
if (points == null || points.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isXAxis)
|
||||
{
|
||||
foreach (var rowGroup in points.GroupBy(point => point.Row))
|
||||
{
|
||||
var ordered = rowGroup.OrderBy(point => point.Col).ToList();
|
||||
for (var i = 1; i < ordered.Count; i++)
|
||||
{
|
||||
var colDelta = ordered[i].Col - ordered[i - 1].Col;
|
||||
if (colDelta <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pitch = Math.Abs((ordered[i].TheoryX - ordered[i - 1].TheoryX) / colDelta);
|
||||
if (pitch > double.Epsilon)
|
||||
{
|
||||
pitchSamples.Add(pitch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var colGroup in points.GroupBy(point => point.Col))
|
||||
{
|
||||
var ordered = colGroup.OrderBy(point => point.Row).ToList();
|
||||
for (var i = 1; i < ordered.Count; i++)
|
||||
{
|
||||
var rowDelta = ordered[i].Row - ordered[i - 1].Row;
|
||||
if (rowDelta <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pitch = Math.Abs((ordered[i].TheoryY - ordered[i - 1].TheoryY) / rowDelta);
|
||||
if (pitch > double.Epsilon)
|
||||
{
|
||||
pitchSamples.Add(pitch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pitchSamples.Count == 0 ? (double?)null : pitchSamples.Average();
|
||||
}
|
||||
|
||||
private static string CreatePointName(int row, int col)
|
||||
{
|
||||
return $"R{row:D2}C{col:D2}";
|
||||
}
|
||||
|
||||
private static bool TryReadCoordinateFile(string filePath, out List<MarkCoordinatePoint> importedPoints, out string errorMessage)
|
||||
{
|
||||
importedPoints = new List<MarkCoordinatePoint>();
|
||||
errorMessage = string.Empty;
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
errorMessage = "坐标文件不存在。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var rawLines = File.ReadAllLines(filePath, Encoding.UTF8)
|
||||
.Where(line => !string.IsNullOrWhiteSpace(line))
|
||||
.ToList();
|
||||
|
||||
if (rawLines.Count == 0)
|
||||
{
|
||||
errorMessage = "坐标文件为空。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var headers = SplitCsvLine(rawLines[0]);
|
||||
var nameIndex = FindColumnIndex(headers, "pointname", "name", "点位名称", "点位名", "point");
|
||||
var rowIndex = FindColumnIndex(headers, "row", "行");
|
||||
var colIndex = FindColumnIndex(headers, "col", "column", "列");
|
||||
var xIndex = FindColumnIndex(headers, "theoryx", "x", "理论x");
|
||||
var yIndex = FindColumnIndex(headers, "theoryy", "y", "理论y");
|
||||
|
||||
if (rowIndex < 0 || colIndex < 0 || xIndex < 0 || yIndex < 0)
|
||||
{
|
||||
errorMessage = "CSV 需至少包含 Row、Col、TheoryX、TheoryY 列。";
|
||||
return false;
|
||||
}
|
||||
|
||||
for (var lineIndex = 1; lineIndex < rawLines.Count; lineIndex++)
|
||||
{
|
||||
var cells = SplitCsvLine(rawLines[lineIndex]);
|
||||
if (cells.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int row;
|
||||
int col;
|
||||
double theoryX;
|
||||
double theoryY;
|
||||
if (!TryParseInt(GetCell(cells, rowIndex), out row)
|
||||
|| !TryParseInt(GetCell(cells, colIndex), out col)
|
||||
|| !TryParseDouble(GetCell(cells, xIndex), out theoryX)
|
||||
|| !TryParseDouble(GetCell(cells, yIndex), out theoryY))
|
||||
{
|
||||
errorMessage = $"第 {lineIndex + 1} 行数据格式无效,请检查 Row、Col、TheoryX、TheoryY。";
|
||||
return false;
|
||||
}
|
||||
|
||||
var pointName = nameIndex >= 0 ? GetCell(cells, nameIndex) : string.Empty;
|
||||
importedPoints.Add(new MarkCoordinatePoint
|
||||
{
|
||||
PointName = string.IsNullOrWhiteSpace(pointName) ? CreatePointName(row, col) : pointName,
|
||||
Row = row,
|
||||
Col = col,
|
||||
TheoryX = theoryX,
|
||||
TheoryY = theoryY,
|
||||
});
|
||||
}
|
||||
|
||||
importedPoints = importedPoints
|
||||
.OrderBy(point => point.Row)
|
||||
.ThenBy(point => point.Col)
|
||||
.ToList();
|
||||
|
||||
for (var i = 0; i < importedPoints.Count; i++)
|
||||
{
|
||||
importedPoints[i].Index = i + 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int FindColumnIndex(IReadOnlyList<string> headers, params string[] candidates)
|
||||
{
|
||||
for (var index = 0; index < headers.Count; index++)
|
||||
{
|
||||
var normalizedHeader = NormalizeHeader(headers[index]);
|
||||
if (candidates.Any(candidate => normalizedHeader == NormalizeHeader(candidate)))
|
||||
{
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static string NormalizeHeader(string value)
|
||||
{
|
||||
return (value ?? string.Empty)
|
||||
.Trim()
|
||||
.Trim('\uFEFF')
|
||||
.Replace(" ", string.Empty)
|
||||
.Replace("_", string.Empty)
|
||||
.Replace("-", string.Empty)
|
||||
.ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static string GetCell(IReadOnlyList<string> cells, int index)
|
||||
{
|
||||
if (index < 0 || index >= cells.Count)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return cells[index]?.Trim() ?? string.Empty;
|
||||
}
|
||||
|
||||
private static bool TryParseInt(string value, out int result)
|
||||
{
|
||||
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out result))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return int.TryParse(value, NumberStyles.Integer, CultureInfo.CurrentCulture, out result);
|
||||
}
|
||||
|
||||
private static bool TryParseDouble(string value, out double result)
|
||||
{
|
||||
if (double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out result))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return double.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.CurrentCulture, out result);
|
||||
}
|
||||
|
||||
private static List<string> SplitCsvLine(string line)
|
||||
{
|
||||
var cells = new List<string>();
|
||||
if (line == null)
|
||||
{
|
||||
return cells;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder();
|
||||
var inQuotes = false;
|
||||
for (var i = 0; i < line.Length; i++)
|
||||
{
|
||||
var current = line[i];
|
||||
if (current == '"')
|
||||
{
|
||||
if (inQuotes && i + 1 < line.Length && line[i + 1] == '"')
|
||||
{
|
||||
builder.Append('"');
|
||||
i++;
|
||||
}
|
||||
else
|
||||
{
|
||||
inQuotes = !inQuotes;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current == ',' && !inQuotes)
|
||||
{
|
||||
cells.Add(builder.ToString());
|
||||
builder.Clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(current);
|
||||
}
|
||||
|
||||
cells.Add(builder.ToString());
|
||||
return cells;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user