Files
test_demo/MX-PD-盘古 - new/PanGu.DieBonderApp/MainShell/Recipe/ViewModel/MarkTeachViewModel.cs
Shi.Ji e31d3560bb 添加 MX-PD-盘古 项目文件
将 MX-PD-盘古 - new 目录下的所有文件添加到主仓库
2026-05-18 11:43:09 +08:00

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
}
}