using Aurora; using Aurora.Api; using Aurora.Api.Devices; using Aurora.Api.Manager; using Aurora.View; using AuroraLaser; using AuroraLaserScanner; using CoreUnit.Scanner.UserControl.ScannerCanvas; using Microsoft.Win32; using MilUI; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Configuration; using System.Diagnostics; using System.IO; using System.Linq; using System.Reactive.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Input; using System.Windows.Threading; using VisionCommon; using WeldingCommon.Sequences; using WeldingCommon.Settings; using WeldingCommon.Utils; namespace WeldingCommon.Views { public class UCScannerCalibrationViewModel : NotifyPropertyChangedBase { private readonly Logger Log = new Logger("UCScannerCalibrationViewModel"); public ICommand CMD_VisibleChanged { get; } public ICommand CMD_ApplyGridItems { get; } public ICommand CMD_SaveDataFile { get; } public ICommand CMD_RunCorreXionPro { get; } public ICommand CMD_RunMarkGrid { get; } public ICommand CMD_StopMarkGrid { get; } public ICommand CMD_RunScannerCalibration { get; } public ICommand CMD_StopScannerCalibration { get; } public ICommand CMD_FindPattern { get; } public ICommand CMD_CalculateDistance { get; } public ICommand CMD_GetCenterCnt { get; } //public ICommand CMD_ChangedCaltype { get; } //public ICommand CMD_SeqStop { get; } private readonly IAuroraEnvironment _Aurora; private SystemParameter _SystemParameter; private readonly Scanner _Scanner; private readonly ILaserDevice _Laser; private readonly ScannerConfig _ScannerConfig; private ISequence _SeqMarkGrid; private bool _IsSequenceRunning_MarkGrid; public bool IsSequenceRunning_MarkGrid { get => _IsSequenceRunning_MarkGrid; set => SetProperty(ref _IsSequenceRunning_MarkGrid, value); } private ISequence _SeqScannerCalibration; private bool _IsSequenceRunning_ScannerCalibration; public bool IsSequenceRunning_ScannerCalibration { get => _IsSequenceRunning_ScannerCalibration; set => SetProperty(ref _IsSequenceRunning_ScannerCalibration, value); } // Support VisionCorrectionList Selection. private ObservableCollection _SelectedVisionCorrectionList; public ObservableCollection SelectedVisionCorrectionList { get => _SelectedVisionCorrectionList; set => SetProperty(ref _SelectedVisionCorrectionList, value); } private ObservableCollection _VisionCorrectionList; public ObservableCollection VisionCorrectionList { get => _VisionCorrectionList; set => SetProperty(ref _VisionCorrectionList, value); } private ObservableCollection _PatternCorrectionList; public ObservableCollection PatternCorrectionList { get => _PatternCorrectionList; set => SetProperty(ref _PatternCorrectionList, value); } // Correction Param private int _GridRowCount; public int GridRowCount { get => _GridRowCount; set => SetProperty(ref _GridRowCount, value); } private int _GridColumnCount; public int GridColumnCount { get => _GridColumnCount; set => SetProperty(ref _GridColumnCount, value); } private float _UnitLength; public float UnitLength { get => _UnitLength; set => SetProperty(ref _UnitLength, value); } private float _TiltingDegree; public float TiltingDegree { get => _TiltingDegree; set => SetProperty(ref _TiltingDegree, value); } private double _CurrentX; public double CurrentX { get => _CurrentX; set => SetProperty(ref _CurrentX, value); } private double _CurrentY; public double CurrentY { get => _CurrentY; set => SetProperty(ref _CurrentY, value); } private bool _Is1StepCalibration; public bool Is1StepCalibration { get => _Is1StepCalibration; set => SetProperty(ref _Is1StepCalibration, value); } private int _RetryNum; public int RetryNum { get => _RetryNum; set => SetProperty(ref _RetryNum, value); } private int _SelectedRowIndex; public int SelectedRowIndex { get => _SelectedRowIndex; set => SetProperty(ref _SelectedRowIndex, value); } // Vision Param private double _MPosOffsetX; public double MPosOffsetX { get => _MPosOffsetX; set => SetProperty(ref _MPosOffsetX, value); } private double _MPosOffsetY; public double MPosOffsetY { get => _MPosOffsetY; set => SetProperty(ref _MPosOffsetY, value); } private double _OffsetThreshold; public double OffsetThreshold { get => _OffsetThreshold; set => SetProperty(ref _OffsetThreshold, value); } // Vision Param - Calibration Type private bool _IsVisionCalibration; public bool IsVisionCalibration { get => _IsVisionCalibration; set => SetProperty(ref _IsVisionCalibration, value); } // Laser Param private double _MarkPower; public double MarkPower { get => _MarkPower; set => SetProperty(ref _MarkPower, value); } private double _MarkSpeed; public double MarkSpeed { get => _MarkSpeed; set => SetProperty(ref _MarkSpeed, value); } private double _MarkLength; public double MarkLength { get => _MarkLength; set => SetProperty(ref _MarkLength, value); } private bool _LaserStatus; public bool LaserStatus { get => _LaserStatus; set => SetProperty(ref _LaserStatus, value); } private bool _IsModulation; public bool IsModulation { get => _IsModulation; set => SetProperty(ref _IsModulation, value); } private uint _Frequency; public uint Frequency { get => _Frequency; set => SetProperty(ref _Frequency, value); } private uint _PulseDuration; public uint PulseDuration { get => _PulseDuration; set => SetProperty(ref _PulseDuration, value); } //Pixel Resolution private bool _IsGrabLiveLimage; public bool IsGrabLiveLimage { get => _IsGrabLiveLimage; set => SetProperty(ref _IsGrabLiveLimage, value); } private double _ReferenceDistance; public double ReferenceDistance { get => _ReferenceDistance; set => SetProperty(ref _ReferenceDistance, value); } private double _PixelDistanceMin; public double PixelDistanceMin { get => _PixelDistanceMin; set => SetProperty(ref _PixelDistanceMin, value); } private double _PixelDistanceMax; public double PixelDistanceMax { get => _PixelDistanceMax; set => SetProperty(ref _PixelDistanceMax, value); } private double _DistanceAverage; public double DistanceAverage { get => _DistanceAverage; set => SetProperty(ref _DistanceAverage, value); } private double _LensMagnification; public double LensMagnification { get => _LensMagnification; set => SetProperty(ref _LensMagnification, value); } private FovInfo _FovInfo; private CameraAgentBase _CameraAgent; private PatternAgentBase _PatternAgent; private object _GrabImage; private PatternResult _PatternResult; private readonly DispatcherTimer _MonitorTimer; //private string _CalibrationType; public UCScannerCalibrationViewModel(IAuroraEnvironment aurora) { _Aurora = aurora; _SystemParameter = aurora.SettingEntry[CommonSettingIds.SystemParameter] as SystemParameter; _Scanner = _Aurora.DeviceManager[CommonDeviceIds.Scanner] as Scanner; _Laser = _Aurora.DeviceManager[CommonDeviceIds.Laser] as ILaserDevice; _ScannerConfig = _Aurora.DeviceManager.GetDeviceConfig(_Scanner.DeviceId) as ScannerConfig; _SeqMarkGrid = _Aurora.SequenceEntry[CommonSequenceIds.SeqMarkGrid] as ISequence; _SeqScannerCalibration = _Aurora.SequenceEntry[CommonSequenceIds.SeqScannerCalibration] as ISequence; _FovInfo = VisionRecipeManager.GetFovInfos().FirstOrDefault(); _CameraAgent = CameraAgentManager.GetCameraAgent(_FovInfo); //FovInfo는 UI 생성자를 통해 전달된 Params의 FovInfo를 사용한다. _PatternAgent = PatternAgentManager.GetAgent(_FovInfo); //GrabImageBuffer를 Update할 필요가 있으면 아래 코드 사용 //IpAdapter.Instance.IpEnvironment.FreeImage(_GrabImage); _GrabImage = _CameraAgent.CloneImageBuffer(); VisionCorrectionList = new ObservableCollection(); SelectedVisionCorrectionList = new ObservableCollection(); PatternCorrectionList = new ObservableCollection(); InitVariable(); CMD_VisibleChanged = new DelegateCommand(VisibleChanged); CMD_ApplyGridItems = new DelegateCommand(PerformCmdApplyGridItems); CMD_SaveDataFile = new DelegateCommand(PerformCmdSaveDataFile); CMD_RunCorreXionPro = new DelegateCommand(PerformCmdRunCorreXionPro); CMD_RunMarkGrid = new DelegateCommand(PerformCmdRunMarkGrid); CMD_StopMarkGrid = new DelegateCommand(PerformCmdStopMarkGrid); CMD_RunScannerCalibration = new DelegateCommand(PerformCmdRunScannerCalibration); CMD_StopScannerCalibration = new DelegateCommand(PerformCmdStopScannerCalibration); CMD_FindPattern = new DelegateCommand(PerformCmdFindPattern); CMD_CalculateDistance = new DelegateCommand(PerformCmdCalculateDistance); CMD_GetCenterCnt = new DelegateCommand(PerformCmdGetCenterCount); _MonitorTimer = new DispatcherTimer(); _MonitorTimer.Tick += OnMonitoring; _MonitorTimer.Interval = TimeSpan.FromMilliseconds(100); // ian0513, 아래 Sequence 에서 보내 주는 걸로만 event 처리 하지 말고, event 등록 해서 처리 되도록 하는 것이 명확함. _Aurora.EventObservable.AsObservable(CommonSequenceIds.SeqScannerCalibration) .ObserveOnDispatcher().Subscribe(x => OnUpdateCorrectionTable(x)); } private void InitVariable() { GridRowCount = 13; GridColumnCount = 13; UnitLength = 10; TiltingDegree = 0; MPosOffsetX = 0; MPosOffsetY = 0; OffsetThreshold = 0.01; IsVisionCalibration = true; MarkPower = 10; MarkSpeed = 100; MarkLength = 10; LaserStatus = false; Frequency = 10000; PulseDuration = 10000; //InitializeDataGridViewItem(GridColumnCount, GridRowCount, UnitLength, TiltingDegree); } private void VisibleChanged(object obj) { bool visible = (bool)obj; if (visible) { _MonitorTimer.Start(); } else { _MonitorTimer.Stop(); } } private void OnMonitoring(object sender, EventArgs e) { double xPos, yPos; if (_Scanner != null) { _Scanner.GetXYPos(out xPos, out yPos); CurrentX = xPos; CurrentY = yPos; } if (_Laser.GetLaserReady()) { LaserStatus = true; } else { LaserStatus = false; } } private void OnUpdateCorrectionTable(EventMessage obj) { var updateDataGridItem = obj.Param as UpdateDataGridItem; if (updateDataGridItem != null) { VisionCorrectionList[updateDataGridItem.Upositionindex].VisionX = updateDataGridItem.UVisionX; VisionCorrectionList[updateDataGridItem.Upositionindex].VisionY = updateDataGridItem.UVisionY; } } private void PerformCmdApplyGridItems(object obj) { // 1. 원래 있던 data table 을 전부 초기화한다 // 2. 새로 적용된 row count 와 column count 로 새로 data table 을 만든다 // 3. VisionX 와 VisionY 는 0값을 넣어둔다. Calibration에서 패턴매칭 값을 이벤트로 받아와서 업데이트한다. try { InitializeDataGridViewItem(GridColumnCount, GridRowCount, UnitLength, TiltingDegree); } catch (Exception ex) { MessageUtil.ShowErrorToast(ex.Message); } } private void PerformCmdSaveDataFile(object obj) { // 저장양식이 정해지면 업데이트하자 // 이전 버전은 database 에 저장하게 되어있음 try { SaveFileDialog dlg = new SaveFileDialog(); dlg.InitialDirectory = @"D:\LASA.eqpdata\Scanner\Correction"; dlg.DefaultExt = "dat"; dlg.Filter = "data 파일 (*.dat)|*.dat"; dlg.Title = "data 파일 저장"; if (dlg.ShowDialog() == true) { using (StreamWriter sw = new StreamWriter(dlg.FileName, false, Encoding.Default)) { string oldCorrectionFile = IsVisionCalibration ? _Scanner.Config.CorrectionFile : _Scanner.Config.LaserCorrectionFile; FileInfo oldCorrectionFileInfo = new FileInfo(oldCorrectionFile); sw.WriteLine(string.Format("OLDCTFILE\t= {0}", oldCorrectionFileInfo.Name)); sw.WriteLine(string.Format("NEWCTFILE\t= NewCorrectionTable.ct5")); sw.WriteLine(string.Format("\r\nTOLERANCE\t= 1.0")); uint newCal = (uint)Math.Round(_Scanner.Config.ScaleXYFactor); sw.WriteLine(string.Format("\r\nNEWCAL\t= {0}", newCal)); //sw.WriteLine(string.Format("LIMITS")); sw.WriteLine(string.Format("\r\n// RTC-X [Bit] RTC-Y[Bit] REAL-X[mm] REAL-Y[mm]\r\n")); double visionX = 0; double visionY = 0; double realX = 0; double realY = 0; double tempX = 0; double tempY = 0; for (int i = 0; i < GridRowCount * GridColumnCount; i++) { visionX = Convert.ToDouble(VisionCorrectionList[i].VisionX); visionY = Convert.ToDouble(VisionCorrectionList[i].VisionY); realX = Convert.ToDouble(VisionCorrectionList[i].MasterX); realY = Convert.ToDouble(VisionCorrectionList[i].MasterY); sw.WriteLine(string.Format("{0}\t{1}\t{2}\t{3}", visionX, visionY, realX, realY)); //string finalX; //string finalY; //if (tempX == 0) //{ // finalX = "0,0"; //} //else //{ // string sX; // string upperX; // string lowerX; // if ((Math.Round(tempX, 3) - Math.Truncate(tempX)) == 0) // { // sX = Convert.ToString(Math.Truncate(tempX)); // upperX = sX; // lowerX = "0"; // } // else // { // //sX = Convert.ToString(Math.Round(tempX, 3)); // sX = Math.Round(tempX, 3).ToString("f3"); // upperX = sX.Substring(0, sX.IndexOf(".")); // lowerX = sX.Substring(sX.IndexOf(".") + 1); // } // finalX = upperX + "," + lowerX; //} //if (tempY == 0) //{ // finalY = "0,0"; //} //else //{ // string sY; // string upperY; // string lowerY; // if ((Math.Round(tempY, 3) - Math.Truncate(tempY)) == 0) // { // sY = Convert.ToString(Math.Truncate(tempY)); // upperY = sY; // lowerY = "0"; // } // else // { // //sY = Convert.ToString(Math.Round(tempY, 3)); // sY = Math.Round(tempY, 3).ToString("f3"); // upperY = sY.Substring(0, sY.IndexOf(".")); // lowerY = sY.Substring(sY.IndexOf(".") + 1); // } // finalY = upperY + "," + lowerY; //} } } } } catch (Exception ex) { Log.E(string.Format(ex.Message)); } } private void PerformCmdRunCorreXionPro(object obj) { var filePath = ConfigurationManager.AppSettings.Get("DATA_PATH"); var fullPath = Path.Combine(filePath, "_device", "_scanner", "correXionPro.exe"); if (File.Exists(fullPath)) { Process.Start(fullPath); } else { Log.I("correXionPro is not exist"); } } private void PerformCmdRunMarkGrid(object obj) { if (_ScannerConfig.UseMarkingOnTheFly == true) { MessageUtil.ShowWarningToast(string.Format("Please Check Marking on the Fly(MOTF)")); return; } if (VisionCorrectionList.Count <= 0 || LaserStatus == false) { MessageUtil.ShowWarningToast(string.Format("Please Check DataCorrection or LaserStatus or LaserCalType")); return; } var result = MessageUtil.ShowYesNoMessageDialog("Laser Calibration", "Start Laser Calibration Sequence?"); if (result == false) { return; } if (_SeqMarkGrid.RunState == SequenceRunState.Running) { MessageUtil.ShowWarningToast(string.Format("이미 시퀀스가 Start 상태입니다.", "_SeqMarkGrid")); return; } var param = new SeqMarkGrid_InitParam() { ColumnCount = GridColumnCount, RowCount = GridRowCount, UnitLength = UnitLength, MarkPower = MarkPower, MarkSpeed = MarkSpeed, MarkLength = MarkLength, IsModulation = IsModulation, Frequnecy = Frequency, PulseDulation = PulseDuration }; IsSequenceRunning_MarkGrid = true; _SeqMarkGrid.Start(param, OnCompleteMarkGrid); } private void OnCompleteMarkGrid(SequenceActionResult res) { if (res != null) { if (res.Param is SeqMarkGrid_OutParam result && result.Success == true) { IsSequenceRunning_MarkGrid = false; MessageUtil.ShowSuccessToast(string.Format("Success SeqMarkGrid")); return; } } IsSequenceRunning_MarkGrid = false; MessageUtil.ShowWarningToast(string.Format("Fail SeqMarkGrid")); } private void PerformCmdStopMarkGrid(object obj) { if (_SeqMarkGrid.RunState == SequenceRunState.Idle) { return; } IsSequenceRunning_MarkGrid = false; _SeqMarkGrid.Stop(); } private void PerformCmdRunScannerCalibration(object obj) { if (_ScannerConfig.UseMarkingOnTheFly == true) { MessageUtil.ShowWarningToast(string.Format("Please Check Marking on the Fly(MOTF)")); return; } if (VisionCorrectionList.Count <= 0) { MessageUtil.ShowWarningToast(string.Format("\"Please Check Data Correction apply")); return; } var result = MessageUtil.ShowYesNoMessageDialog("Scanner Calibration", "Start Scanner Calibration Sequence?\nCheck Calibration Type\nTo calibrate after MarkGrid, CalType set to Laser"); if (result == false) { return; } if (_SeqScannerCalibration.RunState == SequenceRunState.Running) { MessageUtil.ShowWarningToast(string.Format("이미 시퀀스가 Start 상태입니다.", "_SeqScannerCalibration")); return; } var param = new SeqScannerCalibration_InitParam() { FovInfo = _FovInfo, IsVisionCalibration = IsVisionCalibration, ColumnCount = GridColumnCount, RowCount = GridRowCount, UnitLength = UnitLength, TiltingDegree = TiltingDegree, CheckOffset = OffsetThreshold, MasterPosOffsetX = MPosOffsetX, // ian0513, 원본에는 MPosOffsetY 로 되어 있던데... 확인 필요. MasterPosOffsetY = MPosOffsetY, //1step Calibration Param Is1StepCalibration = Is1StepCalibration, RetryNum = RetryNum, SelectedRowIndex = SelectedRowIndex }; IsSequenceRunning_ScannerCalibration = true; _SeqScannerCalibration.Start(param, OnCompleteScannerCalibration); } private void OnCompleteScannerCalibration(SequenceActionResult res) { if (res != null) { if (res.Param is SeqScannerCalibration_OutParam result && result.Success == true) { IsSequenceRunning_ScannerCalibration = false; MessageUtil.ShowSuccessToast(string.Format("Success SeqScannerCalibration")); return; } } IsSequenceRunning_ScannerCalibration = false; MessageUtil.ShowWarningToast(string.Format("Fail SeqScannerCalibration")); } private void PerformCmdStopScannerCalibration(object obj) { if (_SeqScannerCalibration.RunState == SequenceRunState.Idle) { return; } IsSequenceRunning_ScannerCalibration = false; _SeqScannerCalibration.Stop(); } public void MoveScanner() { double scannerPosX = VisionCorrectionList[_SelectedRowIndex].MasterX; double scannerPosY = VisionCorrectionList[SelectedRowIndex].MasterY; var res = MessageUtil.ShowYesNoMessageDialog("Scanner", $"Move Scanner to Postion(X : {scannerPosX}, Y: {scannerPosY}) of Selected Cell?"); if (res) { _Scanner.JumpAbs(scannerPosX, scannerPosY, eFunctionType.COMMAND); } } private void InitializeDataGridViewItem(int columnCount, int rowCount, float unitLength, float tiltingDegree) { try { VisionCorrectionList.Clear(); for (int column = 0; column < columnCount; column++) { for (int row = 0; row < rowCount; row++) { CorrectionUnit dtItem = new CorrectionUnit(); dtItem.Column = column + 1; dtItem.Row = row + 1; float tempX = unitLength * ((column % columnCount) - (columnCount - 1) / 2f); float tempY = unitLength * ((row % rowCount) - (rowCount - 1) / 2f); float masterX = tempX * (float)Math.Cos(tiltingDegree * Math.PI / 180) - tempY * (float)Math.Sin(tiltingDegree * Math.PI / 180); float masterY = tempX * (float)Math.Sin(tiltingDegree * Math.PI / 180) + tempY * (float)Math.Cos(tiltingDegree * Math.PI / 180); dtItem.MasterX = masterX; dtItem.MasterY = masterY; dtItem.VisionX = 0; dtItem.VisionY = 0; VisionCorrectionList.Add(dtItem); } } } catch (Exception) { MessageUtil.CreateAndShowMessageDialog("Correction", "Init Table Fail."); } } private void PerformCmdFindPattern(object obj) { var cameraDisplay = CameraDisplayManager.GetDisplay(_FovInfo, eDisplayType.Pattern) as CameraDisplay; var procPatternInfo = VisionRecipeManager.GetProcPatternInfo(_FovInfo, "Align1", eProcPatternId.ProcPattern1); var patternInfo = _PatternAgent.GetPatternInfo(_FovInfo, procPatternInfo.PatternId); // Live GrabImage 또는 LoadImage 둘다 사용할 수 있도록 한다. if (IsGrabLiveLimage) { _CameraAgent.GrabSync(cameraDisplay, ref _GrabImage); } else { _GrabImage = cameraDisplay.GetImage(); } if (_GrabImage != null) { _PatternResult = _PatternAgent.Find(patternInfo, _GrabImage, cameraDisplay); } else { Log.W("GrabImage is Null"); } if(_PatternResult.Status == eFoundStatus.FoundOK) { var resultsList = _PatternResult.Results.ToList(); PatternCorrectionList.Clear(); for (int i = 0; i < resultsList.Count; i++) { PatternCorrectionUnit patternCorrectionUnit = new PatternCorrectionUnit(); patternCorrectionUnit.Index = i; patternCorrectionUnit.PosX = Math.Round(resultsList[i].ImagePosition.X, 3); patternCorrectionUnit.PosY = Math.Round(resultsList[i].ImagePosition.Y, 3); PatternCorrectionList.Add(patternCorrectionUnit); } _PatternAgent.Draw(patternInfo, _PatternResult, cameraDisplay); } else { Log.W("Pattern is not found."); } } private void PerformCmdCalculateDistance(object obj) { try { List lsDistances = new List(); foreach (var item1 in PatternCorrectionList) { double dDistanceMin = double.MaxValue; foreach (var item2 in PatternCorrectionList) { if (item1 == item2) continue; double dDistance = Math.Sqrt(Math.Pow(item2.PosX - item1.PosX, 2) + Math.Pow(item2.PosY - item1.PosY, 2)); if (dDistance > PixelDistanceMin && dDistance < PixelDistanceMax) lsDistances.Add(dDistance); if (dDistance < dDistanceMin) { dDistanceMin = dDistance; } } if (lsDistances.Count > 0) { item1.Distance = lsDistances.Average(); } else { item1.Distance = dDistanceMin; } } if (PatternCorrectionList.Count > 0) { DistanceAverage = Math.Round(PatternCorrectionList.Average(x => x.Distance), 3); } else { DistanceAverage = 0; } LensMagnification = (DistanceAverage * _SystemParameter.CameraPixelSize) / (ReferenceDistance * 1000.0); } catch (Exception e) { Log.W($"{e.Message}"); } } private void PerformCmdGetCenterCount(object obj) { _Scanner.GetXYPos(out double currentX, out double currentY); // Vision Scanner Calibration인 경우 int visionBitX = (int)(currentX * _Scanner.Config.ScaleXYFactor); int visionBitY = (int)(currentY * _Scanner.Config.ScaleXYFactor); VisionCorrectionList[SelectedRowIndex].VisionX = visionBitX; VisionCorrectionList[SelectedRowIndex].VisionY = visionBitY; } } public class CorrectionUnit : NotifyPropertyChangedBase { private int _Column; public int Column { get => _Column; set => SetProperty(ref _Column, value); } private int _Row; public int Row { get => _Row; set => SetProperty(ref _Row, value); } private float _MasterX; public float MasterX { get => _MasterX; set => SetProperty(ref _MasterX, value); } private float _MasterY; public float MasterY { get => _MasterY; set => SetProperty(ref _MasterY, value); } private int _VisionX; public int VisionX { get => _VisionX; set => SetProperty(ref _VisionX, value); } private int _VisionY; public int VisionY { get => _VisionY; set => SetProperty(ref _VisionY, value); } } public class PatternCorrectionUnit : NotifyPropertyChangedBase { public int Index { get; set; } private double _PosX; public double PosX { get => _PosX; set => SetProperty(ref _PosX, value); } private double _PosY; public double PosY { get => _PosY; set => SetProperty(ref _PosY, value); } // Distance : 가장 가까운 cross line 과의 거리 private double _Distance; public double Distance { get => _Distance; set => SetProperty(ref _Distance, value); } } }