719 lines
26 KiB
C#
719 lines
26 KiB
C#
|
|
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
|
|||
|
|
}
|
|||
|
|
}
|