添加 MX-PD-盘古 项目文件

将 MX-PD-盘古 - new 目录下的所有文件添加到主仓库
This commit is contained in:
Shi.Ji
2026-05-18 11:43:09 +08:00
parent 03632a379d
commit e31d3560bb
739 changed files with 99783 additions and 0 deletions

View File

@@ -0,0 +1,178 @@
using MainShell.Common;
using MainShell.Filewritable;
using MainShell.Models;
using MainShell.Recipe.Models;
using MwFramework.Controls.Components;
using Stylet;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace MainShell.Recipe.ViewModel
{
public class CarrierRecipeViewModel : RecipeViewModelBase<CarrierRecipe>,
IHandle<SubstrateNameChangedEventArgs>, IHandle<SubstrateDeletedEventArgs>
{
#region //Command
public ICommand RemoveSubstrateCommand { get; private set; }
#endregion
private CarrierRecipe _carrierRecipe;
public CarrierRecipe CarrierRecipe
{
get { return _carrierRecipe; }
set { SetAndNotify(ref _carrierRecipe, value); }
}
protected override CarrierRecipe CurrentRecipe
{
get => CarrierRecipe;
set => CarrierRecipe = value;
}
protected override string RecipeFolderPath => Paths.CarrierRecipe;
protected override string EmptyRecipeDisplayName => "当前载具配方";
protected override string GetActiveRecipeName()
{
return RecipeManager.CurrentCarrierRecipe?.RecipeName;
}
protected override void SwitchActiveRecipe(string recipeName)
{
RecipeManager.SwitchCarrierRecipe(recipeName);
}
protected override void ClearActiveRecipe()
{
RecipeManager.ClearCarrierRecipe();
}
public ObservableCollection<RecipeWrap> SubstrateRecipes => _wrapManager.SubstrateRecipeWraps;
private readonly RecipeWrapManager _wrapManager;
private readonly IEventAggregator _eventAggregator;
public CarrierRecipeViewModel(RecipeWrapManager wrapManager, IEventAggregator eventAggregator, RecipeManager recipeManager)
: base(recipeManager)
{
_wrapManager = wrapManager;
_eventAggregator = eventAggregator;
_eventAggregator.Unsubscribe(this);
_eventAggregator.Subscribe(this);
RegisterButtonGroupEvents();
RecipeWraps = wrapManager.CarrierRecipeWraps;
RemoveSubstrateCommand = new DelegateCommand<object>(RemoveSubstrate);
}
protected override void OnViewLoaded()
{
base.OnViewLoaded();
InitializeSelection(RecipeManager.CurrentCarrierRecipe?.RecipeName);
}
protected override void OnRecipeLoaded(CarrierRecipe recipe, RecipeWrap recipeWrap)
{
foreach (var wrap in RecipeWraps)
{
wrap.IsInUse = ReferenceEquals(wrap, recipeWrap);
}
}
public override void OnCopyRecipe(object sender, EventArgs eventArgs)
{
base.OnCopyRecipe(sender, eventArgs);
}
public override void OnImportRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeEventArgs args)
{
}
}
public override void OnSaveRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeEventArgs)
{
SaveCurrentRecipe(true);
}
}
public override void OnExportRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeEventArgs args)
{
}
}
public override void OnReNameRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeRenameEventArgs args)
{
TryRenameRecipe(args);
}
}
#region //Methods
public void AddSubstrate()
{
if (CarrierRecipe == null)
{
MwMessageBox.Show("请先选择或创建载具配方");
return;
}
CarrierRecipe.SubstrateSelectInfos.Add(new SubstrateSelectInfo());
}
public void RemoveSubstrate(object pars)
{
if (pars is SubstrateSelectInfo selectInfo)
{
CarrierRecipe.SubstrateSelectInfos.Remove(selectInfo);
}
}
public void Handle(SubstrateNameChangedEventArgs message)
{
if (CarrierRecipe != null)
{
if (CarrierRecipe.SubstrateSelectInfos != null)
{
foreach (var substrateInfo in CarrierRecipe.SubstrateSelectInfos)
{
if (substrateInfo.SubstrateName == message.OldName)
{
substrateInfo.SubstrateName = message.NewName;
}
}
}
}
}
public void Handle(SubstrateDeletedEventArgs message)
{
if (CarrierRecipe == null || CarrierRecipe.SubstrateSelectInfos == null)
return;
var list = CarrierRecipe.SubstrateSelectInfos;
var targetName = message?.DeletedName;
CommonUti.RunOnUi(() => RemoveByNameFromCollection(list, targetName));
}
private void RemoveByNameFromCollection(ObservableCollection<SubstrateSelectInfo> list, string name)
{
if (string.IsNullOrEmpty(name)) return;
for (int i = list.Count - 1; i >= 0; i--)
{
if (list[i]?.SubstrateName == name)
list.RemoveAt(i);
}
}
#endregion
}
}

View File

@@ -0,0 +1,36 @@
using MainShell.Recipe.Models;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MainShell.Recipe.ViewModel
{
public interface IRecipeViewModel
{
RecipeManager RecipeManager { get; }
ObservableCollection<RecipeWrap> RecipeWraps { get; }
RecipeWrap SelectedRecipeWrap { get; set; }
void SyncSelectionFromActiveRecipe();
RecipeButtonGroupViewModel RecipeButtonGroupViewModel { get;}
bool HideRecipePanel { get; set; }
bool CanCopyRecipe { get; }
bool CanImportRecipe { get; }
bool CanExportRecipe { get; }
bool HasUnsavedChanges { get; }
string CurrentRecipeDisplayName { get; }
bool TryLeaveCurrentRecipeContext();
void SaveCurrentRecipeSilently();
void OnCreateNewRecipe(object sender, EventArgs eventArgs);
void OnDeleteRecipe(object sender, EventArgs eventArgs);
void OnCopyRecipe(object sender, EventArgs eventArgs);
void OnApplyRecipe(object sender, EventArgs eventArgs);
void OnImportRecipe(object sender, EventArgs eventArgs);
void OnSaveRecipe(object sender, EventArgs eventArgs);
void OnExportRecipe(object sender, EventArgs eventArgs);
void OnReNameRecipe(object sender, EventArgs eventArgs);
void OnClearRecipe(object sender, EventArgs eventArgs);
}
}

View File

@@ -0,0 +1,99 @@
using MainShell.Recipe.Models.SubstrateParameter;
using MaxwellFramework.Core.Common.Command;
using Stylet;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;
namespace MainShell.Recipe.ViewModel
{
public class MarkCoordinatePreviewWindowModel : PropertyChangedBase
{
private readonly MarkTeachViewModel _markTeachViewModel;
private ObservableCollection<MarkCoordinatePoint> _observedPoints;
public ObservableCollection<MarkCoordinatePoint> Points => _markTeachViewModel?.CoordinatePreviewPoints;
public int PointCount => Points?.Count ?? 0;
public ICommand CloseCommand { get; }
public MarkCoordinatePreviewWindowModel(MarkTeachViewModel markTeachViewModel)
{
_markTeachViewModel = markTeachViewModel;
CloseCommand = new DelegateCommand(OnClose);
AttachToMarkTeachViewModel();
}
private void OnClose(object obj)
{
if (obj is Window window)
{
window.Close();
}
}
public void RefreshViewState()
{
NotifyOfPropertyChange(() => Points);
NotifyOfPropertyChange(() => PointCount);
}
public void Cleanup()
{
if (_markTeachViewModel != null)
{
_markTeachViewModel.PropertyChanged -= OnMarkTeachViewModelPropertyChanged;
}
DetachPointCollection();
}
private void AttachToMarkTeachViewModel()
{
if (_markTeachViewModel == null)
{
return;
}
_markTeachViewModel.PropertyChanged += OnMarkTeachViewModelPropertyChanged;
AttachPointCollection(_markTeachViewModel.CoordinatePreviewPoints);
}
private void OnMarkTeachViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.PropertyName)
|| e.PropertyName == nameof(MarkTeachViewModel.CoordinatePreviewPoints))
{
DetachPointCollection();
AttachPointCollection(_markTeachViewModel.CoordinatePreviewPoints);
RefreshViewState();
}
}
private void AttachPointCollection(ObservableCollection<MarkCoordinatePoint> points)
{
_observedPoints = points;
if (_observedPoints != null)
{
_observedPoints.CollectionChanged += OnPointsCollectionChanged;
}
}
private void DetachPointCollection()
{
if (_observedPoints != null)
{
_observedPoints.CollectionChanged -= OnPointsCollectionChanged;
_observedPoints = null;
}
}
private void OnPointsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
NotifyOfPropertyChange(() => PointCount);
}
}
}

View File

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

View File

@@ -0,0 +1,108 @@
using MainShell.Recipe.Models;
using Stylet;
namespace MainShell.Recipe.ViewModel
{
public class ProcessCompensationViewModel : PropertyChangedBase
{
private ProcessRecipe _processRecipe;
public ProcessRecipe ProcessRecipe
{
get { return _processRecipe; }
set
{
if (SetAndNotify(ref _processRecipe, value))
{
NotifyCompensationPropertiesChanged();
}
}
}
public double PhsX1Compensation
{
get { return GetCompensationParameter().PhsX1Compensation; }
set
{
ProcessAxisCompensationParameter parameter = GetCompensationParameter();
if (parameter.PhsX1Compensation == value)
{
return;
}
parameter.PhsX1Compensation = value;
NotifyOfPropertyChange(nameof(PhsX1Compensation));
}
}
public double PhsY1Compensation
{
get { return GetCompensationParameter().PhsY1Compensation; }
set
{
ProcessAxisCompensationParameter parameter = GetCompensationParameter();
if (parameter.PhsY1Compensation == value)
{
return;
}
parameter.PhsY1Compensation = value;
NotifyOfPropertyChange(nameof(PhsY1Compensation));
}
}
public double WsXCompensation
{
get { return GetCompensationParameter().WsXCompensation; }
set
{
ProcessAxisCompensationParameter parameter = GetCompensationParameter();
if (parameter.WsXCompensation == value)
{
return;
}
parameter.WsXCompensation = value;
NotifyOfPropertyChange(nameof(WsXCompensation));
}
}
public double StageYCompensation
{
get { return GetCompensationParameter().StageYCompensation; }
set
{
ProcessAxisCompensationParameter parameter = GetCompensationParameter();
if (parameter.StageYCompensation == value)
{
return;
}
parameter.StageYCompensation = value;
NotifyOfPropertyChange(nameof(StageYCompensation));
}
}
private ProcessAxisCompensationParameter GetCompensationParameter()
{
if (ProcessRecipe == null)
{
return new ProcessAxisCompensationParameter();
}
if (ProcessRecipe.ProcessAxisCompensationParameter == null)
{
ProcessRecipe.ProcessAxisCompensationParameter = new ProcessAxisCompensationParameter();
}
return ProcessRecipe.ProcessAxisCompensationParameter;
}
private void NotifyCompensationPropertiesChanged()
{
NotifyOfPropertyChange(nameof(PhsX1Compensation));
NotifyOfPropertyChange(nameof(PhsY1Compensation));
NotifyOfPropertyChange(nameof(WsXCompensation));
NotifyOfPropertyChange(nameof(StageYCompensation));
}
}
}

View File

@@ -0,0 +1,684 @@
using MainShell.Common;
using MainShell.Filewritable;
using MainShell.Hardware;
using MainShell.Log;
using MainShell.Models;
using MainShell.Recipe.Models;
using MainShell.Recipe.Models.PID;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using StyletIoC;
namespace MainShell.Recipe.ViewModel
{
public class ProcessRecipeViewModel : RecipeViewModelBase<ProcessRecipe>
{
private readonly HardwareManager _hardwareManager;
private readonly PIDOperater _pidOperater;
private static readonly string[] _fixedProfileNames = new string[]
{
"默认PID组"
};
private static readonly string[] _fixedCompensationAxisNames = new string[]
{
"PHS-X1",
"PHS-Y1",
"WS-X",
"Stage-Y"
};
private ProcessRecipe _processRecipe;
public ProcessRecipe ProcessRecipe
{
get { return _processRecipe; }
set
{
if (SetAndNotify(ref _processRecipe, value))
{
if (ProcessCompensationViewModel != null)
{
ProcessCompensationViewModel.ProcessRecipe = value;
}
EnsureProcessRecipeState();
SyncProfileSelection();
NotifyOfPropertyChange(() => AxisPIDParameters);
NotifyOfPropertyChange(() => AxisOptions);
NotifyOfPropertyChange(() => SelectedAxes);
NotifyOfPropertyChange(() => SelectedFilteringParameters);
}
}
}
protected override ProcessRecipe CurrentRecipe
{
get { return ProcessRecipe; }
set { ProcessRecipe = value; }
}
protected override string RecipeFolderPath => Paths.ProcessRecipe;
protected override string EmptyRecipeDisplayName => "当前工艺配方";
protected override string GetActiveRecipeName()
{
return RecipeManager.CurrentProcessRecipe != null ? RecipeManager.CurrentProcessRecipe.RecipeName : null;
}
public ObservableCollection<PIDProfile> AxisPIDParameters => ProcessRecipe != null ? ProcessRecipe.AxisPIDParameters : null;
private PIDProfile _selectedPIDProfile;
public PIDProfile SelectedPIDProfile
{
get { return _selectedPIDProfile; }
set
{
if (SetAndNotify(ref _selectedPIDProfile, value))
{
EnsureProfileState(_selectedPIDProfile);
NotifyOfPropertyChange(() => SelectedAxes);
NotifyOfPropertyChange(() => SelectedFilteringParameters);
SyncFilteringSelection();
}
}
}
public ObservableCollection<AxisPIDParameter> SelectedAxes => SelectedPIDProfile != null ? SelectedPIDProfile.Axes : null;
public ObservableCollection<AxisFilteringParameter> SelectedFilteringParameters => SelectedPIDProfile != null ? SelectedPIDProfile.FilteringParameters : null;
private ObservableCollection<PidAxisOption> _axisOptions = new ObservableCollection<PidAxisOption>();
public ObservableCollection<PidAxisOption> AxisOptions
{
get { return _axisOptions; }
set { SetAndNotify(ref _axisOptions, value); }
}
private AxisPIDParameter _selectedAxisParameter;
public AxisPIDParameter SelectedAxisParameter
{
get { return _selectedAxisParameter; }
set { SetAndNotify(ref _selectedAxisParameter, value); }
}
private AxisFilteringParameter _selectedFilteringParameter;
public AxisFilteringParameter SelectedFilteringParameter
{
get { return _selectedFilteringParameter; }
set { SetAndNotify(ref _selectedFilteringParameter, value); }
}
private AxisFilteringParameter _selectedNotchFilteringParameter;
public AxisFilteringParameter SelectedNotchFilteringParameter
{
get { return _selectedNotchFilteringParameter; }
set
{
if (SetAndNotify(ref _selectedNotchFilteringParameter, value) && value != null)
{
SelectedFilteringParameter = value;
}
}
}
private AxisFilteringParameter _selectedLowPassFilteringParameter;
public AxisFilteringParameter SelectedLowPassFilteringParameter
{
get { return _selectedLowPassFilteringParameter; }
set
{
if (SetAndNotify(ref _selectedLowPassFilteringParameter, value) && value != null)
{
SelectedFilteringParameter = value;
}
}
}
private int _selectedParameterTabIndex;
public int SelectedParameterTabIndex
{
get { return _selectedParameterTabIndex; }
set { SetAndNotify(ref _selectedParameterTabIndex, value); }
}
private ProcessCompensationViewModel _processCompensationViewModel;
[Inject]
public ProcessCompensationViewModel ProcessCompensationViewModel
{
get { return _processCompensationViewModel; }
set
{
if (SetAndNotify(ref _processCompensationViewModel, value) && value != null)
{
value.ProcessRecipe = ProcessRecipe;
}
}
}
public ProcessRecipeViewModel(RecipeWrapManager wrapManager, RecipeManager recipeManager, HardwareManager hardwareManager, PIDOperater pidOperater)
: base(recipeManager)
{
_hardwareManager = hardwareManager;
_pidOperater = pidOperater;
RecipeWraps = wrapManager.ProcessRecipeWraps;
RegisterButtonGroupEvents();
AxisOptions = BuildAxisOptions();
PidAxisCatalog.SetAxisOptions(AxisOptions);
}
protected override void OnViewLoaded()
{
base.OnViewLoaded();
AxisOptions = BuildAxisOptions();
PidAxisCatalog.SetAxisOptions(AxisOptions);
InitializeSelection(RecipeManager.CurrentProcessRecipe != null ? RecipeManager.CurrentProcessRecipe.RecipeName : null);
}
protected override void SwitchActiveRecipe(string recipeName)
{
RecipeManager.SwitchProcessRecipe(recipeName);
}
protected override void ClearActiveRecipe()
{
RecipeManager.ClearProcessRecipe();
}
protected override void OnRecipeLoaded(ProcessRecipe recipe, RecipeWrap recipeWrap)
{
EnsureProcessRecipeState();
SyncProfileSelection();
}
public override void OnCopyRecipe(object sender, EventArgs eventArgs)
{
base.OnCopyRecipe(sender, eventArgs);
}
public override void OnImportRecipe(object sender, EventArgs eventArgs)
{
}
public override void OnExportRecipe(object sender, EventArgs eventArgs)
{
}
public override void OnReNameRecipe(object sender, EventArgs eventArgs)
{
RecipeRenameEventArgs args = eventArgs as RecipeRenameEventArgs;
if (args != null)
{
TryRenameRecipe(args);
}
}
public void AddAxis()
{
if (SelectedPIDProfile == null)
{
MwMessageBox.Show("请先选择一个 PID 配置组");
return;
}
EnsureProfileState(SelectedPIDProfile);
PidAxisOption axisOption = GetNextAxisOption(SelectedPIDProfile.Axes.Select(item => item.AxisIndex));
AxisPIDParameter axis = new AxisPIDParameter();
if (axisOption != null)
{
axis.ModuleName = axisOption.AxisName;
}
else
{
axis.ModuleName = string.Format("Axis{0}", SelectedPIDProfile.Axes.Count + 1);
axis.AxisIndex = SelectedPIDProfile.Axes.Count;
}
SelectedPIDProfile.Axes.Add(axis);
SelectedAxisParameter = axis;
NotifyOfPropertyChange(() => SelectedAxes);
NotifyOfPropertyChange(() => HasUnsavedChanges);
}
public void DeleteAxis()
{
if (SelectedPIDProfile == null || SelectedAxisParameter == null)
{
return;
}
EnsureProfileState(SelectedPIDProfile);
int index = SelectedPIDProfile.Axes.IndexOf(SelectedAxisParameter);
SelectedPIDProfile.Axes.Remove(SelectedAxisParameter);
SelectedAxisParameter = SelectedPIDProfile.Axes.Count == 0
? null
: SelectedPIDProfile.Axes[Math.Min(index, SelectedPIDProfile.Axes.Count - 1)];
NotifyOfPropertyChange(() => SelectedAxes);
NotifyOfPropertyChange(() => HasUnsavedChanges);
}
public void AddFilteringParameter()
{
if (SelectedPIDProfile == null)
{
MwMessageBox.Show("请先选择一个 PID 配置组");
return;
}
EnsureProfileState(SelectedPIDProfile);
PidAxisOption axisOption = GetNextAxisOption(SelectedPIDProfile.FilteringParameters.Select(item => item.AxisIndex));
AxisFilteringParameter filteringParameter = new AxisFilteringParameter
{
AxisIndex = axisOption != null ? axisOption.AxisIndex : GetDefaultAxisIndex()
};
SelectedPIDProfile.FilteringParameters.Add(filteringParameter);
SelectedFilteringParameter = filteringParameter;
NotifyOfPropertyChange(() => SelectedFilteringParameters);
NotifyOfPropertyChange(() => HasUnsavedChanges);
}
public void ReadParameters()
{
if (SelectedPIDProfile == null)
{
LocalizedMessageBox.Show(MessageKey.PidNoProfileSelected, MessageKey.TitleWarning);
return;
}
try
{
if (SelectedParameterTabIndex == 1)
{
ReadFilteringParameters();
}
else
{
ReadPidAxisParameters();
}
NotifyOfPropertyChange(() => SelectedAxes);
NotifyOfPropertyChange(() => SelectedFilteringParameters);
NotifyOfPropertyChange(() => HasUnsavedChanges);
LocalizedMessageBox.Show(MessageKey.PidReadSucceeded, MessageKey.TitleInfo);
}
catch (Exception exception)
{
$"PID参数读取失败: {exception}".LogSysError();
LocalizedMessageBox.Show(MessageKey.PidReadFailed, MessageKey.TitleError);
}
}
public void WriteParameters()
{
if (SelectedPIDProfile == null)
{
LocalizedMessageBox.Show(MessageKey.PidNoProfileSelected, MessageKey.TitleWarning);
return;
}
try
{
if (SelectedParameterTabIndex == 1)
{
WriteFilteringParameters();
}
else
{
WritePidAxisParameters();
}
LocalizedMessageBox.Show(MessageKey.PidWriteSucceeded, MessageKey.TitleInfo);
}
catch (Exception exception)
{
$"PID参数写入失败: {exception}".LogSysError();
LocalizedMessageBox.Show(MessageKey.PidWriteFailed, MessageKey.TitleError);
}
}
public void DeleteFilteringParameter()
{
if (SelectedPIDProfile == null || SelectedFilteringParameter == null)
{
return;
}
EnsureProfileState(SelectedPIDProfile);
int index = SelectedPIDProfile.FilteringParameters.IndexOf(SelectedFilteringParameter);
SelectedPIDProfile.FilteringParameters.Remove(SelectedFilteringParameter);
SelectedFilteringParameter = SelectedPIDProfile.FilteringParameters.Count == 0
? null
: SelectedPIDProfile.FilteringParameters[Math.Min(index, SelectedPIDProfile.FilteringParameters.Count - 1)];
NotifyOfPropertyChange(() => SelectedFilteringParameters);
NotifyOfPropertyChange(() => HasUnsavedChanges);
}
private void EnsureProcessRecipeState()
{
if (ProcessRecipe == null)
{
return;
}
if (ProcessRecipe.AxisPIDParameters == null)
{
ProcessRecipe.AxisPIDParameters = new ObservableCollection<PIDProfile>();
}
if (ProcessRecipe.ProcessCompensationParameters == null)
{
ProcessRecipe.ProcessCompensationParameters = new ObservableCollection<ProcessAxisCompensationParameter>();
}
EnsureFixedProfiles();
EnsureFixedCompensationParameters();
foreach (PIDProfile profile in ProcessRecipe.AxisPIDParameters)
{
EnsureProfileState(profile);
}
}
private void SyncProfileSelection()
{
if (AxisPIDParameters == null || AxisPIDParameters.Count == 0)
{
SelectedPIDProfile = null;
SelectedAxisParameter = null;
SelectedFilteringParameter = null;
return;
}
if (SelectedPIDProfile == null || !AxisPIDParameters.Contains(SelectedPIDProfile))
{
SelectedPIDProfile = AxisPIDParameters[0];
}
EnsureProfileState(SelectedPIDProfile);
if (SelectedAxes == null || SelectedAxes.Count == 0)
{
SelectedAxisParameter = null;
}
else if (SelectedAxisParameter == null || !SelectedAxes.Contains(SelectedAxisParameter))
{
SelectedAxisParameter = SelectedAxes[0];
}
SyncFilteringSelection();
}
private void EnsureProfileState(PIDProfile profile)
{
if (profile == null)
{
return;
}
profile.EnsureAxes();
if (profile.FilteringParameters == null)
{
profile.FilteringParameters = new ObservableCollection<AxisFilteringParameter>();
}
if (profile.FilteringParameters.Count == 0)
{
AxisFilteringParameter filteringParameter = new AxisFilteringParameter();
filteringParameter.AxisIndex = GetDefaultAxisIndex();
profile.FilteringParameters.Add(filteringParameter);
}
}
private void SyncFilteringSelection()
{
if (SelectedFilteringParameters == null || SelectedFilteringParameters.Count == 0)
{
SelectedFilteringParameter = null;
SelectedNotchFilteringParameter = null;
SelectedLowPassFilteringParameter = null;
return;
}
if (SelectedFilteringParameter == null || !SelectedFilteringParameters.Contains(SelectedFilteringParameter))
{
SelectedFilteringParameter = SelectedFilteringParameters[0];
}
if (SelectedNotchFilteringParameter == null || !SelectedFilteringParameters.Contains(SelectedNotchFilteringParameter))
{
SelectedNotchFilteringParameter = SelectedFilteringParameter;
}
if (SelectedLowPassFilteringParameter == null || !SelectedFilteringParameters.Contains(SelectedLowPassFilteringParameter))
{
SelectedLowPassFilteringParameter = SelectedFilteringParameter;
}
}
private void EnsureFixedProfiles()
{
foreach (string profileName in _fixedProfileNames)
{
PIDProfile profile = AxisPIDParameters.FirstOrDefault(item => string.Equals(item.Name, profileName, StringComparison.OrdinalIgnoreCase));
if (profile == null)
{
profile = new PIDProfile(profileName);
profile.Description = "程序初始化固定配置组";
AxisPIDParameters.Add(profile);
}
}
}
private void EnsureFixedCompensationParameters()
{
foreach (string axisName in _fixedCompensationAxisNames)
{
ProcessAxisCompensationParameter parameter = ProcessRecipe.ProcessCompensationParameters
.FirstOrDefault(item => string.Equals(item.AxisName, axisName, StringComparison.OrdinalIgnoreCase));
if (parameter == null)
{
parameter = new ProcessAxisCompensationParameter();
parameter.AxisName = axisName;
parameter.CompensationValue = 0d;
ProcessRecipe.ProcessCompensationParameters.Add(parameter);
}
}
}
private void ReadPidAxisParameters()
{
if (SelectedAxes == null)
{
return;
}
foreach (AxisPIDParameter axisParameter in SelectedAxes)
{
AxisPIDParameter deviceParameter = _pidOperater.ReadPIDParametersFromDevice(axisParameter.ModuleName);
CopyAxisParameterValues(deviceParameter, axisParameter);
}
}
private void WritePidAxisParameters()
{
if (SelectedAxes == null)
{
return;
}
foreach (AxisPIDParameter axisParameter in SelectedAxes)
{
_pidOperater.SendPIDParameters(axisParameter);
}
}
private void ReadFilteringParameters()
{
if (SelectedFilteringParameter == null)
{
LocalizedMessageBox.Show(MessageKey.PidNoFilteringParameterSelected, MessageKey.TitleWarning);
return;
}
AxisFilteringParameter deviceParameter = _pidOperater.ReadPIDFilteringParametersFromDevice();
CopyFilteringParameterValues(deviceParameter, SelectedFilteringParameter);
}
private void WriteFilteringParameters()
{
if (SelectedFilteringParameter == null)
{
LocalizedMessageBox.Show(MessageKey.PidNoFilteringParameterSelected, MessageKey.TitleWarning);
return;
}
_pidOperater.SendPIDFilteringParameters(SelectedFilteringParameter);
}
private void CopyAxisParameterValues(AxisPIDParameter source, AxisPIDParameter target)
{
if (source == null || target == null)
{
return;
}
target.PA_SLVKP.Value = source.PA_SLVKP.Value;
target.PA_SLVKI.Value = source.PA_SLVKI.Value;
target.PA_SLVKPSF.Value = source.PA_SLVKPSF.Value;
target.PA_SLVKPIF.Value = source.PA_SLVKPIF.Value;
target.PA_SLVKISF.Value = source.PA_SLVKISF.Value;
target.PA_SLVKIIF.Value = source.PA_SLVKIIF.Value;
target.PA_SLPKP.Value = source.PA_SLPKP.Value;
target.PA_SLPKPIF.Value = source.PA_SLPKPIF.Value;
target.PA_SLPKPSF.Value = source.PA_SLPKPSF.Value;
target.PA_SLAFF.Value = source.PA_SLAFF.Value;
target.PA_SLJFF.Value = source.PA_SLJFF.Value;
}
private void CopyFilteringParameterValues(AxisFilteringParameter source, AxisFilteringParameter target)
{
if (source == null || target == null)
{
return;
}
target.IsUseNotch.Value = source.IsUseNotch.Value;
target.NotchFrequency.Value = source.NotchFrequency.Value;
target.NotchWidth.Value = source.NotchWidth.Value;
target.NotchFalloff.Value = source.NotchFalloff.Value;
target.IsUseFiltering.Value = source.IsUseFiltering.Value;
target.FilteringFrequency.Value = source.FilteringFrequency.Value;
target.FilteringDamping.Value = source.FilteringDamping.Value;
}
private PidAxisOption GetNextAxisOption(IEnumerable<int> usedAxisIndices)
{
if (AxisOptions == null || AxisOptions.Count == 0)
{
return null;
}
HashSet<int> usedAxisSet = new HashSet<int>();
if (usedAxisIndices != null)
{
foreach (int axisIndex in usedAxisIndices)
{
usedAxisSet.Add(axisIndex);
}
}
foreach (PidAxisOption axisOption in AxisOptions)
{
if (!usedAxisSet.Contains(axisOption.AxisIndex))
{
return axisOption;
}
}
return AxisOptions[0];
}
private int GetDefaultAxisIndex()
{
if (AxisOptions == null || AxisOptions.Count == 0)
{
return 0;
}
return AxisOptions[0].AxisIndex;
}
private ObservableCollection<PidAxisOption> BuildAxisOptions()
{
ObservableCollection<PidAxisOption> axisOptions = new ObservableCollection<PidAxisOption>();
if (_hardwareManager != null && _hardwareManager.AxesDic != null && _hardwareManager.AxesDic.Count > 0)
{
int order = 0;
foreach (string axisName in _hardwareManager.AxesDic.Keys.OrderBy(item => item))
{
axisOptions.Add(new PidAxisOption
{
AxisName = axisName,
AxisIndex = ExtractAxisIndex(axisName, order)
});
order++;
}
}
if (axisOptions.Count > 0)
{
return axisOptions;
}
FieldInfo[] fields = typeof(AxisName).GetFields(BindingFlags.Public | BindingFlags.Static);
int fallbackOrder = 0;
foreach (FieldInfo fieldInfo in fields.OrderBy(item => item.Name))
{
object value = fieldInfo.GetValue(null);
string axisName = value as string;
if (string.IsNullOrWhiteSpace(axisName))
{
continue;
}
axisOptions.Add(new PidAxisOption
{
AxisName = axisName,
AxisIndex = ExtractAxisIndex(axisName, fallbackOrder)
});
fallbackOrder++;
}
return axisOptions;
}
private int ExtractAxisIndex(string axisName, int fallbackValue)
{
if (string.IsNullOrWhiteSpace(axisName))
{
return fallbackValue;
}
string digits = new string(axisName.Reverse().TakeWhile(char.IsDigit).Reverse().ToArray());
int axisIndex;
if (int.TryParse(digits, out axisIndex))
{
return axisIndex;
}
return fallbackValue;
}
}
}

View File

@@ -0,0 +1,298 @@
using MainShell.Common;
using MainShell.Models;
using MainShell.Recipe.Models;
using MainShell.Recipe.View;
using MaxwellControl.Tools;
using MaxwellFramework.Core.Common.Command;
using MwFramework.ManagerService;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace MainShell.Recipe.ViewModel
{
public class RecipeButtonGroupViewModel :BaseScreen
{
public ICommand CreateNewCmd { get;private set; }
public ICommand DeleteCmd { get; private set; }
public ICommand CopyCmd { get; private set; }
public ICommand ApplyCmd { get; private set; }
public ICommand ImportCmd { get; private set; }
public ICommand SaveCmd { get; private set; }
public ICommand ExportCmd { get; private set; }
public ICommand ReNameCmd { get; private set; }
public ICommand ClearCmd { get; private set; }
// 新增事件 —— 发布者(按钮组)向外广播意图
public event EventHandler CreateNewRequested;
public event EventHandler DeleteRequested;
public event EventHandler CopyRequested;
public event EventHandler ApplyRequested;
public event EventHandler ImportRequested;
public event EventHandler SaveRequested;
public event EventHandler ExportRequested;
public event EventHandler ReNameRequested;
public event EventHandler ClearRequested;
public RecipeButtonGroupViewModel()
{
CreateNewCmd = new DelegateCommand(obj =>
{
if (obj != null && obj is IRecipeViewModel recipeViewModel)
{
var existingNames = recipeViewModel.RecipeWraps.Select(r => r.RecipeName).ToList();
var recipeNameWindow = new RecipeNameWindowModel
{
ExistingRecipeNames = existingNames
};
ShowRecipeWindow<RecipeNameWindow,RecipeNameWindowModel>(recipeNameWindow, out bool? result);
if (result == true)
{
CreateNewRequested?.Invoke(this, new RecipeEventArgs
{
RecipeName = recipeNameWindow.RecipeName
});
}
}
});
DeleteCmd = new DelegateCommand(obj =>
{
if (obj != null && obj is IRecipeViewModel recipeViewModel)
{
bool flowControl = CheckOperate(recipeViewModel);
if (!flowControl)
{
return;
}
var result = MwMessageBox.Show($"确认删除当前选中配方:{recipeViewModel.SelectedRecipeWrap.RecipeName}?", "确认删除", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (result == MessageBoxResult.Yes)
{
DeleteRequested?.Invoke(this, new RecipeEventArgs
{
RecipeName = recipeViewModel.SelectedRecipeWrap.RecipeName
});
}
}
});
CopyCmd = new DelegateCommand(obj =>
{
if (obj != null && obj is IRecipeViewModel recipeViewModel)
{
if (!EnsureActionSupported(recipeViewModel.CanCopyRecipe, "复制"))
{
return;
}
bool flowControl = CheckOperate(recipeViewModel);
if (!flowControl)
{
return;
}
var existingNames = recipeViewModel.RecipeWraps.Select(r => r.RecipeName).ToList();
var recipeNameWindow = new RecipeNameWindowModel
{
ExistingRecipeNames = existingNames,
RecipeName = recipeViewModel.SelectedRecipeWrap.RecipeName + "_Copy"
};
ShowRecipeWindow<RecipeNameWindow, RecipeNameWindowModel>(recipeNameWindow, out bool? result);
if (result == true)
{
CopyRequested?.Invoke(this, new RecipeEventArgs
{
RecipeName = recipeNameWindow.RecipeName
});
}
}
});
ApplyCmd = new DelegateCommand(obj =>
{
if (obj != null && obj is IRecipeViewModel recipeViewModel)
{
bool flowControl = CheckOperate(recipeViewModel);
if (!flowControl)
{
return;
}
ApplyRequested?.Invoke(this, new RecipeEventArgs
{
RecipeName = recipeViewModel.SelectedRecipeWrap.RecipeName
});
}
});
ImportCmd = new DelegateCommand(obj =>
{
if (obj != null && obj is IRecipeViewModel recipeViewModel)
{
if (!EnsureActionSupported(recipeViewModel.CanImportRecipe, "导入"))
{
return;
}
bool flowControl = CheckOperate(recipeViewModel);
if (!flowControl)
{
return;
}
ImportRequested?.Invoke(this, new RecipeEventArgs
{
RecipeName = recipeViewModel.SelectedRecipeWrap.RecipeName
});
}
});
SaveCmd = new DelegateCommand(obj =>
{
if (obj != null && obj is IRecipeViewModel recipeViewModel)
{
bool flowControl = CheckOperate(recipeViewModel);
if (!flowControl)
{
return;
}
SaveRequested?.Invoke(this, new RecipeEventArgs
{
RecipeName = recipeViewModel.SelectedRecipeWrap.RecipeName
});
}
});
ExportCmd = new DelegateCommand(obj =>
{
if (obj != null && obj is IRecipeViewModel recipeViewModel)
{
if (!EnsureActionSupported(recipeViewModel.CanExportRecipe, "导出"))
{
return;
}
bool flowControl = CheckOperate(recipeViewModel);
if (!flowControl)
{
return;
}
ExportRequested?.Invoke(this, new RecipeEventArgs
{
RecipeName = recipeViewModel.SelectedRecipeWrap.RecipeName
});
}
});
ReNameCmd = new DelegateCommand(obj =>
{
if (obj != null && obj is IRecipeViewModel recipeViewModel)
{
bool flowControl = CheckOperate(recipeViewModel);
if (!flowControl)
{
return;
}
var existingNames = recipeViewModel.RecipeWraps.Select(r => r.RecipeName).ToList();
var recipeNameWindow = new RecipeReNameWindowModel
{
ExistingRecipeNames = existingNames,
OldRecipeName = recipeViewModel.SelectedRecipeWrap.RecipeName
};
ShowRecipeWindow<RecipeReNameWindow,RecipeReNameWindowModel>( recipeNameWindow, out bool? result);
if (result == true)
{
ReNameRequested?.Invoke(this, new RecipeRenameEventArgs
{
NewRecipeName = recipeNameWindow.RecipeName,
OldRecipeName = recipeNameWindow.OldRecipeName
});
}
}
});
ClearCmd = new DelegateCommand(obj =>
{
ClearRequested?.Invoke(this, EventArgs.Empty);
});
}
private static bool CheckOperate(IRecipeViewModel recipeViewModel)
{
if (recipeViewModel.RecipeWraps.Count == 0)
{
MwMessageBox.Show("当前无可操作的配方,请先创建或导入配方。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return false;
}
else if (recipeViewModel.SelectedRecipeWrap == null)
{
MwMessageBox.Show("请先选择一个配方再进行操作。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
return false;
}
return true;
}
private static bool EnsureActionSupported(bool isSupported, string actionName)
{
if (isSupported)
return true;
MwMessageBox.Show($"当前版本暂不支持{actionName}功能。");
return false;
}
private void ShowRecipeWindow<TWindow, TViewModel>(TViewModel viewModel, out bool? result)
where TWindow : Window, new()
{
result = null;
var owner = WindowHelper.GetActiveWindow();
try
{
// 确保在 UI 线程创建/显示窗口
if (Application.Current == null || Application.Current.Dispatcher.CheckAccess())
{
var window = new TWindow
{
DataContext = viewModel,
Owner = owner,
ShowInTaskbar = owner == null,
WindowStartupLocation = owner != null ? WindowStartupLocation.CenterOwner : WindowStartupLocation.CenterScreen
};
// 如果确实需要置顶,可由调用方或窗口自身控制;这里不强制 Topmost
result = window.ShowDialog();
}
else
{
var windowResult= default(bool?);
Application.Current.Dispatcher.Invoke(() =>
{
var window = new TWindow
{
DataContext = viewModel,
Owner = owner,
ShowInTaskbar = owner == null,
WindowStartupLocation = owner != null ? WindowStartupLocation.CenterOwner : WindowStartupLocation.CenterScreen
};
windowResult = window.ShowDialog();
});
result = windowResult;
}
}
catch (Exception)
{
// 避免抛出异常到调用方(视需求可记录日志)
result = null;
}
}
}
}

View File

@@ -0,0 +1,85 @@
using MaxwellFramework.Core.Common.Command;
using MwFramework.ManagerService;
using Stylet;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace MainShell.Recipe.ViewModel
{
public class RecipeNameWindowModel : PropertyChangedBase
{
private bool? _dialogResult;
public bool? DialogResult
{
get => _dialogResult;
set => SetAndNotify(ref _dialogResult, value);
}
private string _recipeName;
public string RecipeName
{
get => _recipeName;
set
{
if(SetAndNotify(ref _recipeName, value))
{
UpdateValidationMessage();
((DelegateCommand)OkCommand).RaiseCanExecuteChanged();
}
}
}
private string _validationMessage;
/// <summary>
/// 在界面上显示的验证信息(为空时不显示)
/// </summary>
public string ValidationMessage
{
get => _validationMessage;
set => SetAndNotify(ref _validationMessage, value);
}
public IEnumerable<string> ExistingRecipeNames { get; set; }
public ICommand OkCommand { get; }
public ICommand CancelCommand { get; }
public RecipeNameWindowModel()
{
OkCommand = new DelegateCommand(OnOk, CanOk);
CancelCommand = new DelegateCommand(OnCancel);
}
private void OnOk(object obj)
{
DialogResult = true;
}
private bool CanOk(object obj)
{
if (string.IsNullOrWhiteSpace(RecipeName))
return false;
if (ExistingRecipeNames != null && ExistingRecipeNames.Contains(RecipeName, StringComparer.OrdinalIgnoreCase))
return false;
return true;
}
private void OnCancel(object obj)
{
DialogResult = false;
}
private void UpdateValidationMessage()
{
if (string.IsNullOrWhiteSpace(RecipeName))
{
ValidationMessage = "名称不能为空";
return;
}
if (ExistingRecipeNames != null && ExistingRecipeNames.Contains(RecipeName, StringComparer.OrdinalIgnoreCase))
{
ValidationMessage = "名称已存在";
return;
}
ValidationMessage = string.Empty;
}
}
}

View File

@@ -0,0 +1,93 @@
using MaxwellFramework.Core.Common.Command;
using Stylet;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace MainShell.Recipe.ViewModel
{
public class RecipeReNameWindowModel : PropertyChangedBase
{
private bool? _dialogResult;
public bool? DialogResult
{
get => _dialogResult;
set => SetAndNotify(ref _dialogResult, value);
}
private string _recipeName;
public string RecipeName
{
get => _recipeName;
set
{
if (SetAndNotify(ref _recipeName, value))
{
UpdateValidationMessage();
((DelegateCommand)OkCommand).RaiseCanExecuteChanged();
}
}
}
private string _oldRecipeName;
public string OldRecipeName
{
get { return _oldRecipeName; }
set { SetAndNotify(ref _oldRecipeName, value); }
}
private string _validationMessage;
/// <summary>
/// 在界面上显示的验证信息(为空时不显示)
/// </summary>
public string ValidationMessage
{
get => _validationMessage;
set => SetAndNotify(ref _validationMessage, value);
}
public IEnumerable<string> ExistingRecipeNames { get; set; }
public ICommand OkCommand { get; }
public ICommand CancelCommand { get; }
public RecipeReNameWindowModel()
{
OkCommand = new DelegateCommand(OnOk, CanOk);
CancelCommand = new DelegateCommand(OnCancel);
}
private void OnOk(object obj)
{
DialogResult = true;
}
private bool CanOk(object obj)
{
if (string.IsNullOrWhiteSpace(RecipeName))
return false;
if (ExistingRecipeNames != null && ExistingRecipeNames.Contains(RecipeName, StringComparer.OrdinalIgnoreCase))
return false;
return true;
}
private void OnCancel(object obj)
{
DialogResult = false;
}
private void UpdateValidationMessage()
{
if (string.IsNullOrWhiteSpace(RecipeName))
{
ValidationMessage = "名称不能为空";
return;
}
if (ExistingRecipeNames != null && ExistingRecipeNames.Contains(RecipeName, StringComparer.OrdinalIgnoreCase))
{
ValidationMessage = "名称已存在";
return;
}
ValidationMessage = string.Empty;
}
}
}

View File

@@ -0,0 +1,264 @@
using MainShell.Common;
using MainShell.Models;
using MainShell.Recipe.Models;
using MaxwellFramework.Core.Interfaces;
using MwFramework.ManagerService;
using Stylet;
using StyletIoC;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MainShell.Recipe.ViewModel
{
public class RecipeViewModel :BaseScreen ,IPage
{
private bool _isReactivatingAfterBlockedLeave;
public string Name => "Recipe";
private const string CARRIER = "载具配方";
private const string SUBSTRATE = "基板配方";
private const string WAFER = "芯片配方";
private const string PROCESS = "工艺配方";
private readonly Dictionary<string, BaseScreen> _viewModelDict = new Dictionary<string, BaseScreen>();
public ObservableCollection<MenuItemWrap> MenuItemWraps { get; private set; }
private MenuItemWrap _selectedMenuItem;
public MenuItemWrap SelectedMenuItem
{
get { return _selectedMenuItem; }
set
{
if (_selectedMenuItem == value)
return;
if (_selectedMenuItem != null && value != null && !TryLeaveCurrentScreen())
{
NotifyOfPropertyChange(() => SelectedMenuItem);
return;
}
if (SetAndNotify(ref _selectedMenuItem, value))
{
CurrentScreen = value != null ? _viewModelDict[value.Header] : null;
SyncCurrentScreenSelectionFromActiveRecipe();
}
}
}
private BaseScreen _currentScreen;
public BaseScreen CurrentScreen
{
get { return _currentScreen; }
set
{
if (_currentScreen == value)
return;
UnsubscribeScreenState(_currentScreen as INotifyPropertyChanged);
if (SetAndNotify(ref _currentScreen, value))
{
SubscribeScreenState(value as INotifyPropertyChanged);
NotifyOfPropertyChange(() => IsRecipePanelHidden);
}
}
}
public bool IsRecipePanelHidden
{
get
{
var recipeScreen = CurrentScreen as IRecipeViewModel;
return recipeScreen != null && recipeScreen.HideRecipePanel;
}
set
{
var recipeScreen = CurrentScreen as IRecipeViewModel;
if (recipeScreen == null || recipeScreen.HideRecipePanel == value)
return;
recipeScreen.HideRecipePanel = value;
NotifyOfPropertyChange(() => IsRecipePanelHidden);
}
}
private CarrierRecipeViewModel _carrierRecipeViewModel;
[Inject]
public CarrierRecipeViewModel CarrierRecipeViewModel
{
get { return _carrierRecipeViewModel; }
set { _carrierRecipeViewModel = value; }
}
private SubstrateRecipeViewModel _substrateRecipeViewModel;
[Inject]
public SubstrateRecipeViewModel SubstrateRecipeViewModel
{
get { return _substrateRecipeViewModel; }
set { _substrateRecipeViewModel = value; }
}
private WaferRecipeViewModel _waferRecipeViewModel;
[Inject]
public WaferRecipeViewModel WaferRecipeViewModel
{
get { return _waferRecipeViewModel; }
set { _waferRecipeViewModel = value; }
}
private ProcessRecipeViewModel _processRecipeViewModel;
[Inject]
public ProcessRecipeViewModel ProcessRecipeViewModel
{
get { return _processRecipeViewModel; }
set { _processRecipeViewModel = value; }
}
public RecipeViewModel()
{
InitMenuItems();
}
protected override void OnViewLoaded()
{
base.OnViewLoaded();
EnsureViewModelDict();
if(SelectedMenuItem==null)
SelectedMenuItem = MenuItemWraps[0];
SyncCurrentScreenSelectionFromActiveRecipe();
}
private void InitMenuItems()
{
MenuItemWraps = new ObservableCollection<MenuItemWrap>
{
//new MenuItemWrap() { Header=CARRIER,Tag=CARRIER },
new MenuItemWrap() { Header=SUBSTRATE,Tag=SUBSTRATE },
new MenuItemWrap() { Header=WAFER,Tag=WAFER },
new MenuItemWrap() { Header=PROCESS,Tag=PROCESS },
};
}
private void InitViewModelDict()
{
_viewModelDict.Clear();
_viewModelDict.Add(CARRIER, CarrierRecipeViewModel);
_viewModelDict.Add(SUBSTRATE, SubstrateRecipeViewModel);
_viewModelDict.Add(WAFER, WaferRecipeViewModel);
_viewModelDict.Add(PROCESS, ProcessRecipeViewModel);
}
private void EnsureViewModelDict()
{
if (_viewModelDict.Count == 0)
{
InitViewModelDict();
}
}
private void SyncCurrentScreenSelectionFromActiveRecipe()
{
var recipeScreen = CurrentScreen as IRecipeViewModel;
recipeScreen?.SyncSelectionFromActiveRecipe();
}
private bool TryLeaveCurrentScreen()
{
var guard = CurrentScreen as IRecipeViewModel;
if (guard == null)
return true;
return guard.TryLeaveCurrentRecipeContext();
}
public bool TrySelectMenuItem(MenuItemWrap targetMenuItem)
{
if (targetMenuItem == null)
return false;
SelectedMenuItem = targetMenuItem;
return SelectedMenuItem == targetMenuItem;
}
public bool TrySelectRecipeWrap(RecipeWrap targetRecipeWrap)
{
var guard = CurrentScreen as IRecipeViewModel;
if (guard == null || targetRecipeWrap == null)
return false;
if (guard.SelectedRecipeWrap == targetRecipeWrap)
return true;
guard.SelectedRecipeWrap = targetRecipeWrap;
return guard.SelectedRecipeWrap == targetRecipeWrap;
}
private void SubscribeScreenState(INotifyPropertyChanged screen)
{
if (screen != null)
{
screen.PropertyChanged += OnCurrentScreenPropertyChanged;
}
}
private void UnsubscribeScreenState(INotifyPropertyChanged screen)
{
if (screen != null)
{
screen.PropertyChanged -= OnCurrentScreenPropertyChanged;
}
}
private void OnCurrentScreenPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (string.IsNullOrWhiteSpace(e.PropertyName) || string.Equals(e.PropertyName, nameof(BaseScreen.HideRecipePanel), StringComparison.Ordinal))
{
NotifyOfPropertyChange(() => IsRecipePanelHidden);
}
}
protected override void OnStateChanged(ScreenState previousState, ScreenState newState)
{
base.OnStateChanged(previousState, newState);
if (_isReactivatingAfterBlockedLeave)
return;
if (newState == ScreenState.Active)
{
EnsureViewModelDict();
if (SelectedMenuItem == null && MenuItemWraps != null && MenuItemWraps.Count > 0)
{
SelectedMenuItem = MenuItemWraps[0];
}
SyncCurrentScreenSelectionFromActiveRecipe();
}
if (previousState == ScreenState.Active && newState == ScreenState.Deactivated && !TryLeaveCurrentScreen())
{
try
{
_isReactivatingAfterBlockedLeave = true;
Execute.OnUIThread(() => ((IScreenState)this).Activate());
}
finally
{
_isReactivatingAfterBlockedLeave = false;
}
}
}
protected override void OnDeactivate()
{
UnsubscribeScreenState(_currentScreen as INotifyPropertyChanged);
base.OnDeactivate();
}
}
}

View File

@@ -0,0 +1,442 @@
using MainShell.Common;
using MainShell.Models;
using MainShell.Recipe.Models;
using Newtonsoft.Json.Linq;
using Stylet;
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
namespace MainShell.Recipe.ViewModel
{
public abstract class RecipeViewModelBase<TRecipe> : BaseScreen, IRecipeViewModel
where TRecipe : RecipeBase, new()
{
private string _lastSavedSnapshot = string.Empty;
protected string LastSavedSnapshot => _lastSavedSnapshot;
private bool _skipLeaveCheck;
private RecipeWrap _selectedRecipeWrap;
public ObservableCollection<RecipeWrap> RecipeWraps { get; protected set; } = new ObservableCollection<RecipeWrap>();
public RecipeWrap SelectedRecipeWrap
{
get { return _selectedRecipeWrap; }
set
{
if (_selectedRecipeWrap == value)
return;
if (!_skipLeaveCheck && _selectedRecipeWrap != null && value != null && !TryLeaveCurrentRecipeContext())
{
NotifyOfPropertyChange(() => SelectedRecipeWrap);
return;
}
if (SetAndNotify(ref _selectedRecipeWrap, value))
{
LoadRecipeFromWrap(value);
}
}
}
public RecipeButtonGroupViewModel RecipeButtonGroupViewModel { get; protected set; }
public virtual bool CanCopyRecipe => true;
public virtual bool CanImportRecipe => false;
public virtual bool CanExportRecipe => false;
public bool HasUnsavedChanges => IsRecipeDirty();
public string CurrentRecipeDisplayName => CurrentRecipe != null ? CurrentRecipe.RecipeName : EmptyRecipeDisplayName;
public RecipeManager RecipeManager { get; }
protected RecipeViewModelBase(RecipeManager recipeManager)
{
RecipeManager = recipeManager;
}
protected abstract TRecipe CurrentRecipe { get; set; }
protected abstract string RecipeFolderPath { get; }
protected abstract string EmptyRecipeDisplayName { get; }
protected abstract string GetActiveRecipeName();
protected abstract void SwitchActiveRecipe(string recipeName);
protected abstract void ClearActiveRecipe();
protected virtual void OnRecipeLoaded(TRecipe recipe, RecipeWrap recipeWrap) { }
protected virtual string GetUnsavedChangesMessage() => $"<22><EFBFBD><E4B7BD>{CurrentRecipeDisplayName}<7D><><EFBFBD><EFBFBD>δ<EFBFBD><CEB4><EFBFBD><EFBFBD><EFBFBD>޸ģ<DEB8><C4A3>Ƿ<EFBFBD><C7B7>ȱ<EFBFBD><C8B1>棿";
protected virtual void AfterDeleteCurrentRecipe(string deletedRecipeName) { }
protected virtual void AfterRecipeRenamed(string oldRecipeName, string newRecipeName) { }
public virtual bool TryLeaveCurrentRecipeContext()
{
if (!HasUnsavedChanges)
return true;
var result = MwMessageBox.Show(GetUnsavedChangesMessage(), <><CEB4><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʾ", System.Windows.MessageBoxButton.YesNoCancel, System.Windows.MessageBoxImage.Warning);
if (result == System.Windows.MessageBoxResult.Cancel)
return false;
if (result == System.Windows.MessageBoxResult.Yes)
SaveCurrentRecipe(false);
return true;
}
public void SaveCurrentRecipeSilently()
{
SaveCurrentRecipe(false);
}
protected void RegisterButtonGroupEvents()
{
RecipeButtonGroupViewModel = new RecipeButtonGroupViewModel();
RecipeButtonGroupViewModel.CreateNewRequested -= OnCreateNewRecipe;
RecipeButtonGroupViewModel.ApplyRequested -= OnApplyRecipe;
RecipeButtonGroupViewModel.DeleteRequested -= OnDeleteRecipe;
RecipeButtonGroupViewModel.CopyRequested -= OnCopyRecipe;
RecipeButtonGroupViewModel.ImportRequested -= OnImportRecipe;
RecipeButtonGroupViewModel.SaveRequested -= OnSaveRecipe;
RecipeButtonGroupViewModel.ExportRequested -= OnExportRecipe;
RecipeButtonGroupViewModel.ReNameRequested -= OnReNameRecipe;
RecipeButtonGroupViewModel.ClearRequested -= OnClearRecipe;
RecipeButtonGroupViewModel.CreateNewRequested += OnCreateNewRecipe;
RecipeButtonGroupViewModel.ApplyRequested += OnApplyRecipe;
RecipeButtonGroupViewModel.DeleteRequested += OnDeleteRecipe;
RecipeButtonGroupViewModel.CopyRequested += OnCopyRecipe;
RecipeButtonGroupViewModel.ImportRequested += OnImportRecipe;
RecipeButtonGroupViewModel.SaveRequested += OnSaveRecipe;
RecipeButtonGroupViewModel.ExportRequested += OnExportRecipe;
RecipeButtonGroupViewModel.ReNameRequested += OnReNameRecipe;
RecipeButtonGroupViewModel.ClearRequested += OnClearRecipe;
}
public virtual void OnCreateNewRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeEventArgs args)
{
if (!TryLeaveCurrentRecipeContext())
return;
var recipeWrap = new RecipeWrap
{
RecipeName = args.RecipeName,
CreateTime = DateTime.UtcNow,
ModifiedTime = DateTime.UtcNow,
};
RecipeWraps.Add(recipeWrap);
SelectRecipeWrapInternal(recipeWrap);
}
}
public virtual void OnDeleteRecipe(object sender, EventArgs eventArgs)
{
if (SelectedRecipeWrap == null)
return;
if (!TryLeaveCurrentRecipeContext())
return;
var deletedRecipeName = SelectedRecipeWrap.RecipeName;
if (!RecipeManager.DeleteRecipeFolder(RecipeFolderPath, deletedRecipeName, out var error))
{
MwMessageBox.Show(error);
return;
}
RecipeWraps.Remove(SelectedRecipeWrap);
AfterDeleteCurrentRecipe(deletedRecipeName);
if (RecipeWraps.Count > 0)
{
SelectRecipeWrapInternal(RecipeWraps[0]);
}
else
{
SelectedRecipeWrap = null;
CurrentRecipe = null;
ClearActiveRecipe();
CaptureSavedSnapshot();
}
}
public virtual void OnCopyRecipe(object sender, EventArgs eventArgs)
{
if (!(eventArgs is RecipeEventArgs args) || SelectedRecipeWrap == null)
return;
var sourceRecipeName = SelectedRecipeWrap.RecipeName;
if (string.IsNullOrWhiteSpace(sourceRecipeName) || string.IsNullOrWhiteSpace(args.RecipeName))
return;
if (CurrentRecipe != null &&
string.Equals(CurrentRecipe.RecipeName, sourceRecipeName, StringComparison.Ordinal) &&
HasUnsavedChanges)
{
SaveCurrentRecipe(false);
}
if (!RecipeManager.CopyRecipeFolder(RecipeFolderPath, sourceRecipeName, args.RecipeName, out var error))
{
MwMessageBox.Show(error);
return;
}
var copiedRecipeWrap = new RecipeWrap
{
RecipeName = args.RecipeName,
CreateTime = DateTime.UtcNow,
ModifiedTime = DateTime.UtcNow,
};
RecipeWraps.Add(copiedRecipeWrap);
SelectRecipeWrapInternal(copiedRecipeWrap);
}
public virtual void OnApplyRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeEventArgs args)
{
var recipeWrap = RecipeWraps.FirstOrDefault(p => p.RecipeName == args.RecipeName);
if (recipeWrap == null)
return;
if (!ReferenceEquals(SelectedRecipeWrap, recipeWrap))
{
if (!TryLeaveCurrentRecipeContext())
return;
SelectRecipeWrapInternal(recipeWrap);
}
if (SelectedRecipeWrap == null)
return;
if (CurrentRecipe != null && IsRecipeDirty())
{
SaveCurrentRecipe(false);
}
SwitchActiveRecipe(SelectedRecipeWrap.RecipeName);
UpdateInUseState(SelectedRecipeWrap);
}
}
public abstract void OnImportRecipe(object sender, EventArgs eventArgs);
public virtual void OnSaveRecipe(object sender, EventArgs eventArgs)
{
CommonUti.RunOnUi(() =>
{
if (eventArgs is RecipeEventArgs)
{
SaveCurrentRecipe(true);
}
});
}
public abstract void OnExportRecipe(object sender, EventArgs eventArgs);
public abstract void OnReNameRecipe(object sender, EventArgs eventArgs);
public virtual void OnClearRecipe(object sender, EventArgs eventArgs)
{
if (!TryLeaveCurrentRecipeContext())
return;
if (SelectedRecipeWrap == null)
return;
var clearedRecipe = new TRecipe
{
RecipeName = SelectedRecipeWrap.RecipeName
};
CurrentRecipe = clearedRecipe;
OnRecipeLoaded(clearedRecipe, SelectedRecipeWrap);
NotifyOfPropertyChange(() => HasUnsavedChanges);
}
protected bool IsRecipeDirty()
{
if (CurrentRecipe == null)
return false;
var path = Path.Combine(CurrentRecipe.Dir ?? string.Empty, CurrentRecipe.FileName ?? string.Empty);
if (!File.Exists(path))
return true;
return !AreSnapshotsEquivalent(_lastSavedSnapshot, CurrentRecipe.CreateSnapshot());
}
protected void CaptureSavedSnapshot()
{
_lastSavedSnapshot = CurrentRecipe != null ? CurrentRecipe.CreateSnapshot() : string.Empty;
NotifyOfPropertyChange(() => HasUnsavedChanges);
NotifyOfPropertyChange(() => CurrentRecipeDisplayName);
}
protected bool TryRenameRecipe(RecipeRenameEventArgs args)
{
if (args == null)
return false;
var recipeWrap = RecipeWraps.FirstOrDefault(p => p.RecipeName == args.OldRecipeName);
if (recipeWrap == null)
return false;
if (!RecipeManager.RenameFolder(RecipeFolderPath, args.OldRecipeName, args.NewRecipeName, out var error))
{
MwMessageBox.Show(error);
return false;
}
recipeWrap.RecipeName = args.NewRecipeName;
recipeWrap.ModifiedTime = DateTime.UtcNow;
if (CurrentRecipe != null && string.Equals(CurrentRecipe.RecipeName, args.OldRecipeName, StringComparison.Ordinal))
{
CurrentRecipe.RecipeName = args.NewRecipeName;
}
if (ReferenceEquals(recipeWrap, SelectedRecipeWrap))
{
SwitchActiveRecipe(args.NewRecipeName);
}
CaptureSavedSnapshot();
AfterRecipeRenamed(args.OldRecipeName, args.NewRecipeName);
return true;
}
protected void SaveCurrentRecipe(bool showSuccessMessage)
{
if (CurrentRecipe == null)
return;
CurrentRecipe.Write();
CaptureSavedSnapshot();
if (SelectedRecipeWrap != null)
{
SelectedRecipeWrap.ModifiedTime = DateTime.UtcNow;
}
if (showSuccessMessage)
{
MwMessageBox.Show("<22><><EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD>");
}
}
private static bool AreSnapshotsEquivalent(string leftSnapshot, string rightSnapshot)
{
var left = string.IsNullOrWhiteSpace(leftSnapshot) ? null : ParseSnapshotToken(leftSnapshot);
var right = string.IsNullOrWhiteSpace(rightSnapshot) ? null : ParseSnapshotToken(rightSnapshot);
if (left == null || right == null)
return false;
return JToken.DeepEquals(left, right);
}
private static JToken ParseSnapshotToken(string snapshot)
{
try
{
return JToken.Parse(snapshot);
}
catch
{
return null;
}
}
protected void SelectRecipeWrapInternal(RecipeWrap recipeWrap)
{
try
{
_skipLeaveCheck = true;
SelectedRecipeWrap = recipeWrap;
}
finally
{
_skipLeaveCheck = false;
}
}
protected void InitializeSelection(string activeRecipeName)
{
SyncSelection(activeRecipeName, true);
}
protected virtual void UpdateInUseState(RecipeWrap activeRecipeWrap)
{
foreach (var wrap in RecipeWraps)
{
wrap.IsInUse = ReferenceEquals(wrap, activeRecipeWrap);
}
}
public void SyncSelectionFromActiveRecipe()
{
SyncSelection(GetActiveRecipeName(), false);
}
private void SyncSelection(string recipeName, bool fallbackToFirst)
{
if (RecipeWraps == null || RecipeWraps.Count == 0)
{
UpdateInUseState(null);
return;
}
var target = string.IsNullOrWhiteSpace(recipeName)
? null
: RecipeWraps.FirstOrDefault(p => string.Equals(p.RecipeName, recipeName, StringComparison.Ordinal));
if (target == null)
{
UpdateInUseState(null);
if (!fallbackToFirst)
return;
target = RecipeWraps[0];
}
if (ReferenceEquals(SelectedRecipeWrap, target))
{
UpdateInUseState(target);
return;
}
SelectRecipeWrapInternal(target);
}
private void LoadRecipeFromWrap(RecipeWrap recipeWrap)
{
if (recipeWrap == null)
{
UpdateInUseStateByActiveRecipe();
return;
}
var newRecipe = new TRecipe
{
RecipeName = recipeWrap.RecipeName
};
newRecipe.Read();
CurrentRecipe = newRecipe;
CaptureSavedSnapshot();
UpdateInUseStateByActiveRecipe();
OnRecipeLoaded(newRecipe, recipeWrap);
}
private void UpdateInUseStateByActiveRecipe()
{
var activeRecipeName = GetActiveRecipeName();
var activeRecipeWrap = RecipeWraps == null || string.IsNullOrWhiteSpace(activeRecipeName)
? null
: RecipeWraps.FirstOrDefault(p => string.Equals(p.RecipeName, activeRecipeName, StringComparison.Ordinal));
UpdateInUseState(activeRecipeWrap);
}
}
}

View File

@@ -0,0 +1,177 @@
using MainShell.Hardware;
using MainShell.Models;
using MainShell.Recipe.Models;
using MainShell.Recipe.Models.SubstrateParameter;
using MaxwellFramework.Core.Common.Command;
using Stylet;
using System.Linq;
using System.Windows.Input;
namespace MainShell.Recipe.ViewModel
{
public class SubstrateHeightMeasureViewModel : PropertyChangedBase
{
private readonly HardwareManager _hardwareManager;
public SubstrateHeightMeasureViewModel(HardwareManager hardwareManager)
{
_hardwareManager = hardwareManager;
AddPointCmd = new DelegateCommand(AddPoint);
DeletePointCmd = new DelegateCommand(DeletePoint);
TeachPointCmd = new DelegateCommand(TeachPoint);
ApplyOffsetToAllCmd = new DelegateCommand(ApplyOffsetToAll);
}
private SubstrateRecipe _substrateRecipe;
public SubstrateRecipe SubstrateRecipe
{
get { return _substrateRecipe; }
set
{
if (SetAndNotify(ref _substrateRecipe, value))
{
NotifyOfPropertyChange(() => HeightMeasureSetting);
NotifyOfPropertyChange(() => CommonOffsetCompensation);
NotifyOfPropertyChange(() => Points);
NotifyOfPropertyChange(() => Mode);
NotifyOfPropertyChange(() => IsTeachMode);
NotifyOfPropertyChange(() => IsRowColumnMode);
}
}
}
public SubstrateHeightMeasureSetting HeightMeasureSetting => SubstrateRecipe?.HeightMeasureSetting;
public SubstrateHeightMeasureMode Mode
{
get { return HeightMeasureSetting == null ? SubstrateHeightMeasureMode.StandardTeachPosition : HeightMeasureSetting.Mode; }
set
{
if (HeightMeasureSetting == null || HeightMeasureSetting.Mode == value)
{
return;
}
HeightMeasureSetting.Mode = value;
NotifyOfPropertyChange(() => Mode);
NotifyOfPropertyChange(() => IsTeachMode);
NotifyOfPropertyChange(() => IsRowColumnMode);
}
}
public bool IsTeachMode
{
get { return Mode == SubstrateHeightMeasureMode.StandardTeachPosition; }
set
{
if (value)
{
Mode = SubstrateHeightMeasureMode.StandardTeachPosition;
}
}
}
public bool IsRowColumnMode
{
get { return Mode == SubstrateHeightMeasureMode.RowColumnOffset; }
set
{
if (value)
{
Mode = SubstrateHeightMeasureMode.RowColumnOffset;
}
}
}
public System.Collections.ObjectModel.ObservableCollection<SubstrateHeightMeasurePoint> Points => HeightMeasureSetting?.Points;
public MPoint CommonOffsetCompensation => HeightMeasureSetting?.CommonOffsetCompensation;
private SubstrateHeightMeasurePoint _selectedPoint;
public SubstrateHeightMeasurePoint SelectedPoint
{
get { return _selectedPoint; }
set { SetAndNotify(ref _selectedPoint, value); }
}
public ICommand AddPointCmd { get; }
public ICommand DeletePointCmd { get; }
public ICommand TeachPointCmd { get; }
public ICommand ApplyOffsetToAllCmd { get; }
private void ApplyOffsetToAll(object obj)
{
if (Points == null || CommonOffsetCompensation == null)
{
return;
}
foreach (var point in Points)
{
if (point.OffsetCompensation == null)
{
point.OffsetCompensation = new MPoint();
}
point.OffsetCompensation.X = CommonOffsetCompensation.X;
point.OffsetCompensation.Y = CommonOffsetCompensation.Y;
}
}
private void AddPoint(object obj)
{
if (Points == null)
{
return;
}
var point = new SubstrateHeightMeasurePoint
{
PointName = $"Point{Points.Count + 1}",
TeachPosition = new MPoint(),
OffsetCompensation = new MPoint()
};
Points.Add(point);
SelectedPoint = point;
}
private void DeletePoint(object obj)
{
if (Points == null || SelectedPoint == null)
{
return;
}
Points.Remove(SelectedPoint);
SelectedPoint = Points.LastOrDefault();
}
private void TeachPoint(object obj)
{
if (SelectedPoint == null)
{
return;
}
var device = _hardwareManager.CameraAxisManager.TopCameraAxisDevices.FirstOrDefault();
if (device == null)
{
return;
}
if (SelectedPoint.TeachPosition == null)
{
SelectedPoint.TeachPosition = new MPoint();
}
if (device.AxisX != null)
{
SelectedPoint.TeachPosition.X = device.AxisX.State.ActualPos;
}
if (device.AxisY != null)
{
SelectedPoint.TeachPosition.Y = device.AxisY.State.ActualPos;
}
}
}
}

View File

@@ -0,0 +1,319 @@
using MainShell.Common;
using MainShell.Filewritable;
using MainShell.Models;
using MainShell.Recipe.Models;
using MainShell.Recipe.Services;
using MaxwellFramework.Core.Common.Command;
using Newtonsoft.Json.Linq;
using Stylet;
using StyletIoC;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Input;
namespace MainShell.Recipe.ViewModel
{
public class SubstrateRecipeViewModel : RecipeViewModelBase<SubstrateRecipe>, IHandle<WaferNameChangedEventArgs>
{
private readonly RecipeWrapManager _wrapManager;
private readonly IEventAggregator _eventAggregator;
private SubstrateRecipe _substrateRecipe;
public SubstrateRecipe SubstrateRecipe
{
get { return _substrateRecipe; }
set
{
if (SetAndNotify(ref _substrateRecipe, value))
{
if (SubstrateTeachViewModel != null)
{
SubstrateTeachViewModel.SubstrateRecipe = value;
}
if (SubstrateHeightMeasureViewModel != null)
{
SubstrateHeightMeasureViewModel.SubstrateRecipe = value;
}
}
}
}
protected override SubstrateRecipe CurrentRecipe
{
get => SubstrateRecipe;
set => SubstrateRecipe = value;
}
protected override string RecipeFolderPath => Paths.SubstrateRecipe;
protected override string EmptyRecipeDisplayName => "当前基板配方";
protected override string GetActiveRecipeName()
{
return RecipeManager.CurrentSubstrateRecipe?.RecipeName;
}
protected override void SwitchActiveRecipe(string recipeName)
{
RecipeManager.SwitchSubstrateRecipe(recipeName);
}
protected override void ClearActiveRecipe()
{
RecipeManager.ClearSubstrateRecipe();
}
private int _selectedTabIndex;
/// <summary>
/// 当前选中的 Tab 索引0=基础设置显示配方列表1=参数示教(隐藏配方列表)
/// </summary>
public int SelectedTabIndex
{
get => _selectedTabIndex;
set
{
if (SetAndNotify(ref _selectedTabIndex, value))
HideRecipePanel = value != 0;
}
}
private SubstrateTeachViewModel _substrateTeachViewModel;
[Inject]
public SubstrateTeachViewModel SubstrateTeachViewModel
{
get { return _substrateTeachViewModel; }
set
{
if (SetAndNotify(ref _substrateTeachViewModel, value) && value != null)
{
value.SubstrateRecipe = SubstrateRecipe;
}
}
}
private SubstrateHeightMeasureViewModel _substrateHeightMeasureViewModel;
[Inject]
public SubstrateHeightMeasureViewModel SubstrateHeightMeasureViewModel
{
get { return _substrateHeightMeasureViewModel; }
set
{
if (SetAndNotify(ref _substrateHeightMeasureViewModel, value) && value != null)
{
value.SubstrateRecipe = SubstrateRecipe;
}
}
}
public ObservableCollection<RecipeWrap> WaferRecipes => _wrapManager.WaferRecipeWraps;
public SubstrateRecipeViewModel(RecipeWrapManager wrapManager, IEventAggregator eventAggregator, RecipeManager recipeManager)
: base(recipeManager)
{
_wrapManager = wrapManager;
RecipeWraps = wrapManager.SubstrateRecipeWraps;
RegisterButtonGroupEvents();
_eventAggregator = eventAggregator;
_eventAggregator.Unsubscribe(this);
_eventAggregator.Subscribe(this);
DeleteSelectWaferInfoCmd = new DelegateCommand(DeleteSelectWaferInfo);
}
#region//Commands
public ICommand DeleteSelectWaferInfoCmd { get; private set; }
#endregion
protected override string GetUnsavedChangesMessage()
{
var differenceSummary = GetRecipeDifferenceSummary();
return string.IsNullOrWhiteSpace(differenceSummary)
? $"配方“{CurrentRecipeDisplayName}”有未保存修改,是否先保存?"
: $"配方“{CurrentRecipeDisplayName}”有未保存修改:\r\n{differenceSummary}\r\n是否先保存";
}
protected override void OnViewLoaded()
{
base.OnViewLoaded();
InitializeSelection(RecipeManager.CurrentSubstrateRecipe?.RecipeName);
}
protected override void OnRecipeLoaded(SubstrateRecipe recipe, RecipeWrap recipeWrap)
{
}
protected override void AfterDeleteCurrentRecipe(string deletedRecipeName)
{
RecipeManager.DeleteSubstrateRecipe(deletedRecipeName);
_eventAggregator.Publish(new SubstrateDeletedEventArgs
{
DeletedName = deletedRecipeName,
});
}
public override void OnCopyRecipe(object sender, EventArgs eventArgs)
{
base.OnCopyRecipe(sender, eventArgs);
}
public override void OnImportRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeEventArgs args)
{
}
}
public override void OnExportRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeEventArgs args)
{
}
}
public override void OnReNameRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeRenameEventArgs args)
{
if (TryRenameRecipe(args))
{
_eventAggregator.Publish(new SubstrateNameChangedEventArgs
{
OldName = args.OldRecipeName,
NewName = args.NewRecipeName,
});
}
}
}
public void Handle(WaferNameChangedEventArgs message)
{
}
private string GetRecipeDifferenceSummary()
{
if (SubstrateRecipe == null)
return string.Empty;
var path = System.IO.Path.Combine(SubstrateRecipe.Dir ?? string.Empty, SubstrateRecipe.FileName ?? string.Empty);
if (!System.IO.File.Exists(path))
return "1. 当前配方文件尚未保存到磁盘";
var currentSnapshot = SubstrateRecipe.CreateSnapshot();
if (string.Equals(LastSavedSnapshot ?? string.Empty, currentSnapshot, StringComparison.Ordinal))
return string.Empty;
try
{
var originalToken = JToken.Parse(LastSavedSnapshot ?? string.Empty);
var currentToken = JToken.Parse(currentSnapshot);
var differences = new List<string>();
CollectDifferences(originalToken, currentToken, string.Empty, differences, 6);
if (differences.Count == 0)
return "1. 当前配方内容与上次保存快照不一致";
return string.Join("\r\n", differences);
}
catch
{
return "1. 当前配方内容与上次保存快照不一致";
}
}
private void CollectDifferences(JToken originalToken, JToken currentToken, string path, IList<string> differences, int maxCount)
{
if (differences.Count >= maxCount)
return;
if (JToken.DeepEquals(originalToken, currentToken))
return;
if (originalToken == null || currentToken == null)
{
differences.Add($"{differences.Count + 1}. {FormatDifference(path, originalToken, currentToken)}");
return;
}
if (originalToken.Type == JTokenType.Object && currentToken.Type == JTokenType.Object)
{
var originalObject = (JObject)originalToken;
var currentObject = (JObject)currentToken;
var propertyNames = originalObject.Properties().Select(p => p.Name)
.Union(currentObject.Properties().Select(p => p.Name))
.Distinct()
.ToList();
foreach (var propertyName in propertyNames)
{
var nextPath = string.IsNullOrWhiteSpace(path) ? propertyName : $"{path}.{propertyName}";
CollectDifferences(originalObject[propertyName], currentObject[propertyName], nextPath, differences, maxCount);
if (differences.Count >= maxCount)
return;
}
return;
}
if (originalToken.Type == JTokenType.Array && currentToken.Type == JTokenType.Array)
{
var originalArray = (JArray)originalToken;
var currentArray = (JArray)currentToken;
var maxLength = Math.Max(originalArray.Count, currentArray.Count);
for (var i = 0; i < maxLength; i++)
{
var nextPath = $"{path}[{i}]";
var originalItem = i < originalArray.Count ? originalArray[i] : null;
var currentItem = i < currentArray.Count ? currentArray[i] : null;
CollectDifferences(originalItem, currentItem, nextPath, differences, maxCount);
if (differences.Count >= maxCount)
return;
}
return;
}
differences.Add($"{differences.Count + 1}. {FormatDifference(path, originalToken, currentToken)}");
}
private string FormatDifference(string path, JToken originalToken, JToken currentToken)
{
var displayPath = string.IsNullOrWhiteSpace(path) ? "配方根节点" : path;
return $"{displayPath}{FormatTokenValue(originalToken)} -> {FormatTokenValue(currentToken)}";
}
private string FormatTokenValue(JToken token)
{
if (token == null || token.Type == JTokenType.Null || token.Type == JTokenType.Undefined)
return "空";
var value = token.ToString(Newtonsoft.Json.Formatting.None);
if (value.Length > 80)
return value.Substring(0, 80) + "...";
return value;
}
#region//Methods
public void AddWaferSelectInfo()
{
if (SubstrateRecipe == null)
{
MwMessageBox.Show("请先选择或创建基板配方");
return;
}
SubstrateRecipe.WaferSelectInfos.Add(new Models.SubstrateParameter.WaferSelectInfo());
}
private void DeleteSelectWaferInfo(object obj)
{
if (obj is Models.SubstrateParameter.WaferSelectInfo info)
{
SubstrateRecipe.WaferSelectInfos.Remove(info);
}
}
#endregion
}
}

View File

@@ -0,0 +1,53 @@
using MainShell.Hardware;
using MainShell.Recipe.Models;
using MainShell.Recipe.Services;
using Stylet;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
namespace MainShell.Recipe.ViewModel
{
public class SubstrateTeachViewModel : CameraBaseViewModel
{
private SubstrateRecipe _substrateRecipe;
public SubstrateRecipe SubstrateRecipe
{
get { return _substrateRecipe; }
set
{
if (SetAndNotify(ref _substrateRecipe, value))
{
if (MarkTeachViewModel != null)
{
MarkTeachViewModel.SubstrateRecipe = value;
}
}
}
}
private MarkTeachViewModel _markTeachViewModel;
public MarkTeachViewModel MarkTeachViewModel
{
get { return _markTeachViewModel; }
set { SetAndNotify(ref _markTeachViewModel, value); }
}
private readonly HardwareManager _hardwareManager;
private readonly IWindowManager _windowManager;
public SubstrateTeachViewModel(HardwareManager hardwareManager, IWindowManager windowManager)
{
_hardwareManager = hardwareManager ?? throw new ArgumentNullException(nameof(hardwareManager));
_windowManager = windowManager ?? throw new ArgumentNullException(nameof(windowManager));
_cameraAxisViewModel = IoC.Get<Common.Display.ViewModel.CameraAxisViewModel>();
MarkTeachViewModel = new MarkTeachViewModel(hardwareManager, _windowManager);
_cameraAxisViewModel.CameraAxisDevices.HardwareDeviceList = hardwareManager.CameraAxisManager.TopCameraAxisDevices;
}
}
}

View File

@@ -0,0 +1,249 @@
using MainShell.Filewritable;
using MainShell.Models;
using MainShell.Recipe.Models;
using Stylet;
using StyletIoC;
using System;
using System.Collections.ObjectModel;
using System.Linq;
namespace MainShell.Recipe.ViewModel
{
public class WaferRecipeViewModel : RecipeViewModelBase<WaferRecipe>, IHandle<ProcessNameChangedEventArgs>
{
private WaferRecipe _waferRecipe;
public WaferRecipe WaferRecipe
{
get { return _waferRecipe; }
set
{
if (SetAndNotify(ref _waferRecipe, value))
{
if (WaferTeachViewModel != null)
{
WaferTeachViewModel.WaferRecipe = value;
}
}
}
}
protected override WaferRecipe CurrentRecipe
{
get => WaferRecipe;
set => WaferRecipe = value;
}
protected override string RecipeFolderPath => Paths.WaferRecipe;
protected override string EmptyRecipeDisplayName => "当前芯片配方";
protected override string GetActiveRecipeName()
{
return RecipeManager.CurrentWaferRecipe?.RecipeName;
}
private bool _skipProcessSelectionWriteBack;
private int _selectedTabIndex;
public int SelectedTabIndex
{
get { return _selectedTabIndex; }
set
{
if (SetAndNotify(ref _selectedTabIndex, value))
{
HideRecipePanel = value != 0;
}
}
}
private WaferTeachViewModel _waferTeachViewModel;
[Inject]
public WaferTeachViewModel WaferTeachViewModel
{
get { return _waferTeachViewModel; }
set
{
if (SetAndNotify(ref _waferTeachViewModel, value) && value != null)
{
value.WaferRecipe = WaferRecipe;
}
}
}
private RecipeWrap _selectedProcessRecipeWrap;
public RecipeWrap SelectedProcessRecipeWrap
{
get { return _selectedProcessRecipeWrap; }
set
{
if (SetAndNotify(ref _selectedProcessRecipeWrap, value))
{
if (!_skipProcessSelectionWriteBack && WaferRecipe != null)
{
WaferRecipe.ProcessRecipeName = value != null ? value.RecipeName : string.Empty;
NotifyOfPropertyChange(() => HasUnsavedChanges);
}
}
}
}
public ObservableCollection<RecipeWrap> ProcessRecipes => _wrapManager.ProcessRecipeWraps;
private readonly RecipeWrapManager _wrapManager;
private readonly IEventAggregator _eventAggregator;
public WaferRecipeViewModel(RecipeWrapManager wrapManager, IEventAggregator eventAggregator, RecipeManager recipeManager)
: base(recipeManager)
{
_wrapManager = wrapManager;
_eventAggregator = eventAggregator;
RegisterButtonGroupEvents();
RecipeWraps = wrapManager.WaferRecipeWraps;
_eventAggregator.Unsubscribe(this);
_eventAggregator.Subscribe(this);
}
protected override void OnViewLoaded()
{
base.OnViewLoaded();
InitializeSelection(RecipeManager.CurrentWaferRecipe?.RecipeName);
SelectedTabIndex = HideRecipePanel ? 1 : 0;
}
protected override void SwitchActiveRecipe(string recipeName)
{
RecipeManager.SwitchWaferRecipe(recipeName);
}
protected override void ClearActiveRecipe()
{
RecipeManager.ClearWaferRecipe();
}
protected override void OnRecipeLoaded(WaferRecipe recipe, RecipeWrap recipeWrap)
{
SyncProcessSelectionFromWaferRecipe();
}
protected override void AfterDeleteCurrentRecipe(string deletedRecipeName)
{
_eventAggregator.Publish(new WaferDeletedEventArgs
{
DeletedName = deletedRecipeName,
});
}
protected override void AfterRecipeRenamed(string oldRecipeName, string newRecipeName)
{
_eventAggregator.Publish(new WaferNameChangedEventArgs
{
OldName = oldRecipeName,
NewName = newRecipeName,
});
SyncProcessSelectionFromWaferRecipe();
}
public override void OnCopyRecipe(object sender, EventArgs eventArgs)
{
base.OnCopyRecipe(sender, eventArgs);
}
public override void OnImportRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeEventArgs args)
{
}
}
public override void OnExportRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeEventArgs args)
{
}
}
public override void OnReNameRecipe(object sender, EventArgs eventArgs)
{
if (eventArgs is RecipeRenameEventArgs args)
{
TryRenameRecipe(args);
}
}
public void Handle(ProcessNameChangedEventArgs message)
{
if (message == null || string.IsNullOrWhiteSpace(message.OldName) || string.IsNullOrWhiteSpace(message.NewName))
return;
foreach (RecipeWrap wrap in RecipeWraps)
{
WaferRecipe recipe = new WaferRecipe
{
RecipeName = wrap.RecipeName
};
recipe.Read();
if (string.Equals(recipe.ProcessRecipeName, message.OldName, StringComparison.Ordinal))
{
recipe.ProcessRecipeName = message.NewName;
recipe.Write();
}
}
if (WaferRecipe != null && string.Equals(WaferRecipe.ProcessRecipeName, message.OldName, StringComparison.Ordinal))
{
WaferRecipe.ProcessRecipeName = message.NewName;
CaptureSavedSnapshot();
}
SyncProcessSelectionFromWaferRecipe();
}
private void SyncProcessSelectionFromWaferRecipe()
{
if (WaferRecipe == null || ProcessRecipes == null)
{
SetSelectedProcessRecipeWrapInternal(null);
return;
}
if (string.IsNullOrWhiteSpace(WaferRecipe.ProcessRecipeName))
{
SetSelectedProcessRecipeWrapInternal(null);
return;
}
RecipeWrap processWrap = ProcessRecipes.FirstOrDefault(p => p.RecipeName == WaferRecipe.ProcessRecipeName);
if (processWrap != null)
{
SetSelectedProcessRecipeWrapInternal(processWrap);
return;
}
SetSelectedProcessRecipeWrapInternal(null);
}
private void SetSelectedProcessRecipeWrapInternal(RecipeWrap recipeWrap)
{
try
{
_skipProcessSelectionWriteBack = true;
SelectedProcessRecipeWrap = recipeWrap;
}
finally
{
_skipProcessSelectionWriteBack = false;
}
}
public void MakeTemplate()
{
if (WaferTeachViewModel != null)
{
WaferTeachViewModel.MakeTemplate();
}
}
}
}

View File

@@ -0,0 +1,167 @@
using MainShell.Common;
using MainShell.Common.Display.ViewModel;
using MainShell.Common.ViewModel;
using MainShell.Common.VisionTemple.ViewModel;
using MainShell.Hardware;
using MainShell.Log;
using MainShell.Models;
using MainShell.Motion;
using MainShell.Recipe.Models;
using MwFramework.Device;
using MwFramework.ManagerService;
using Stylet;
using System;
using System.Windows;
namespace MainShell.Recipe.ViewModel
{
public class WaferTeachViewModel : CameraBaseViewModel
{
private WaferRecipe _waferRecipe;
private readonly HardwareManager _hardwareManager;
private readonly IWindowManager _windowManager;
private readonly SafeAxisMotion _safeAxisMotion;
private readonly VisionTempleWindowViewModel _visionTempleWindowViewModel;
public WaferRecipe WaferRecipe
{
get { return _waferRecipe; }
set { SetAndNotify(ref _waferRecipe, value); }
}
public WaferTeachViewModel(
HardwareManager hardwareManager,
IWindowManager windowManager,
SafeAxisMotion safeAxisMotion,
VisionTempleWindowViewModel visionTempleWindowViewModel,
CameraAxisViewModel cameraAxisViewModel)
{
_hardwareManager = hardwareManager ?? throw new ArgumentNullException(nameof(hardwareManager));
_windowManager = windowManager ?? throw new ArgumentNullException(nameof(windowManager));
_safeAxisMotion = safeAxisMotion ?? throw new ArgumentNullException(nameof(safeAxisMotion));
_visionTempleWindowViewModel = visionTempleWindowViewModel ?? throw new ArgumentNullException(nameof(visionTempleWindowViewModel));
CameraAxisViewModel = cameraAxisViewModel ?? throw new ArgumentNullException(nameof(cameraAxisViewModel));
CameraAxisViewModel.CameraAxisDevices.HardwareDeviceList = _hardwareManager.CameraAxisManager.TopCameraWsAxisDevices;
}
public void TeachStartPoint()
{
try
{
IAxis axisX;
IAxis axisY;
if (!TryGetScanAxes(out axisX, out axisY))
{
return;
}
WaferRecipe.ScanSettings.StartPointX = GetAxisPosition(axisX);
WaferRecipe.ScanSettings.StartPointY = GetAxisPosition(axisY);
}
catch (Exception ex)
{
LogManager.LogSysError(ex);
ShowOperationFailed(ex.Message);
}
}
public void MoveToStartPoint()
{
try
{
IAxis axisX;
IAxis axisY;
if (!TryGetScanAxes(out axisX, out axisY))
{
return;
}
_safeAxisMotion.SafeMove(
MotionMoveRequest.ForAxis(axisX, WaferRecipe.ScanSettings.StartPointX),
MotionMoveRequest.ForAxis(axisY, WaferRecipe.ScanSettings.StartPointY));
}
catch (Exception ex)
{
LogManager.LogSysError(ex);
ShowOperationFailed(ex.Message);
}
}
public void OpenVisionParameterSetting()
{
CameraConfig cameraConfig = new CameraConfig();
UpCamLightConfig lightConfig = new UpCamLightConfig();
CameraSettingsViewModel settingsViewModel = new CameraSettingsViewModel(_windowManager, _hardwareManager);
settingsViewModel.Initialize(cameraConfig, lightConfig, CameraType.TopWsCamera);
_windowManager.ShowDialog(settingsViewModel);
}
public void MakeTemplate()
{
try
{
_visionTempleWindowViewModel.ShowWindow();
}
catch (Exception ex)
{
LogManager.LogSysError(ex);
LocalizedMessageBox.ShowFormat(
MessageKey.OriginCalibOpenVisionTemplateFailed,
MessageKey.TitleError,
MessageBoxButton.OK,
MessageBoxImage.Error,
ex.Message);
}
}
private bool TryGetScanAxes(out IAxis axisX, out IAxis axisY)
{
axisX = null;
axisY = null;
if (WaferRecipe == null || WaferRecipe.ScanSettings == null)
{
return false;
}
if (CameraAxisViewModel == null || CameraAxisViewModel.CameraAxisDevices == null || CameraAxisViewModel.CameraAxisDevices.SelectedHardwareDevice == null)
{
LocalizedMessageBox.Show(MessageKey.DeviceNotInitialized, MessageKey.TitleWarning, MessageBoxButton.OK, MessageBoxImage.Warning);
return false;
}
HardwareDevice selectedHardwareDevice = CameraAxisViewModel.CameraAxisDevices.SelectedHardwareDevice;
axisX = selectedHardwareDevice.AxisX;
axisY = selectedHardwareDevice.AxisY;
if (axisX == null || axisY == null)
{
LocalizedMessageBox.Show(MessageKey.DeviceNotInitialized, MessageKey.TitleWarning, MessageBoxButton.OK, MessageBoxImage.Warning);
return false;
}
return true;
}
private static double GetAxisPosition(IAxis axis)
{
if (axis == null)
{
throw new ArgumentNullException(nameof(axis));
}
return axis.State != null ? axis.State.ActualPos : axis.GetPositionImmediate();
}
private static void ShowOperationFailed(string message)
{
string title = LanguageResourceHelper.GetString(MessageKey.TitleError);
if (string.IsNullOrWhiteSpace(message))
{
MwMessageBox.Show(LanguageResourceHelper.GetString(MessageKey.CommonOperationFailed), title, MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
MwMessageBox.Show(message, title, MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}