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 AvailableTemplates { get; set; } = new ObservableCollection(); 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 MarkDatas => MarkPars?.MarkDatas; public MarkCoordinateGenerationState CoordinateGenerationState => MarkPars?.CoordinateGenerationState; public ObservableCollection 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(); 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 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(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() .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 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(); 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 points, bool isXAxis) { var pitchSamples = new List(); 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 importedPoints, out string errorMessage) { importedPoints = new List(); 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 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 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 SplitCsvLine(string line) { var cells = new List(); 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 } }