using CoreUnit.Control.UserControl.Canvas; using ILSOS_PrismaticGun.Process; using ILSOS_PrismaticGun.Recipes; using ILSOS_PrismaticGun.Views.Recipe; using ILSOS_PrismaticGun.WeldingCanvas.Popup; using MahApps.Metro.Controls; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.Drawing.Printing; using System.Linq; using System.Reactive; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Converters; using System.Windows.Media.Media3D; using System.Windows.Threading; using VisionCommon; using WeldingCommon; namespace ILSOS_PrismaticGun.WeldingCanvas { public class WeldingCanvas : Canvas { private double canvasW; // 캔버스 Height private double canvasH; // 캔버스 Width private const double margin = 20; // 캔버스 테두리 - margin - 도형 - maring - 캔버스 테두리 // 1. 원본 도형 바운딩 박스 계산 //private double minX; // 도형 사이즈 //private double maxX; // 도형 사이즈 //private double minY; // 도형 사이즈 //private double maxY; // 도형 사이즈 //private double shapeW; // 도형 사이즈 //private double shapeH; // 도형 사이즈 private Rect rc; // << Main Welder에서 사용되는 도형은 사각형이라... 그냥 Rect를 사용하도록 함, 추후 변경이 필요하면 수정해야됨 // 2. 캔버스 내 최대 크기 확보 private double availableShapeW; private double availableShapeH; // 3. 비율 유지하면서 도형 확대 (가로/세로 중 작은 쪽 기준) private double scaleX; private double scaleY; private double shapeScale; private double scaledWidth; private double scaledHeight; protected readonly Pen _penCanCapLayout; protected readonly Pen _penCanCaphightlightLayout; protected readonly Pen _penNumberText; protected readonly Brush _brushCanCapLayout; protected readonly Brush _brushCanCapNGLayout; protected double radius = 3.0; protected double CanCapCenterX; protected double CanCapCenterY; protected List refLaserCanCapPosUnits; private DispatcherTimer _timer; private bool _isWelding; public WeldingCanvas() { _brushCanCapLayout = new SolidColorBrush(Color.FromArgb(255, 46, 173, 165)); _brushCanCapNGLayout = new SolidColorBrush(Color.FromArgb(255, 255, 0, 0)); _penCanCapLayout = new Pen(Brushes.Black, 2); _penCanCaphightlightLayout = new Pen(Brushes.Red, 3); _timer = new System.Windows.Threading.DispatcherTimer(); _timer.Interval = TimeSpan.FromSeconds(1); _timer.Tick += OnTick; IsVisibleChanged += WeldingCanvas_IsVisibleChanged; Loaded += WeldingCanvas_Loaded; PreviewMouseLeftButtonDown += WeldingCanvas_PreviewMouseLeftButtonDown; } private void WeldingCanvas_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (e.ClickCount == 2) { var popup = new PopupWinCellInfo(PalletInfo.WeldingStage == eStage.A ? ProcessManager.Instance.PalletInStageA : ProcessManager.Instance.PalletInStageB); popup.Owner = Application.Current.MainWindow; popup.WindowStartupLocation = WindowStartupLocation.CenterOwner; popup.Show(); e.Handled = true; } } private void WeldingCanvas_Loaded(object sender, RoutedEventArgs e) { //PalletInfo.PalletStateChanged += OnCellStateChanged; } private void OnCellStateChanged(object sender, ePalletState e) { _isWelding = e == ePalletState.Welding; Dispatcher.BeginInvoke(new Action(InvalidateVisual), DispatcherPriority.Render); } private void WeldingCanvas_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { } private void OnTick(object sender, EventArgs e) { } #region Canvas Draw Object public MainRecipe MainRcp { get => (MainRecipe)GetValue(MainRcpProperty); set => SetValue(MainRcpProperty, value); } public static readonly DependencyProperty MainRcpProperty = DependencyProperty.Register(nameof(MainRcp), typeof(MainRecipe), typeof(WeldingCanvas), new PropertyMetadata(new MainRecipe(), OnMainRecipeChanged)); private static void OnMainRecipeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var canvas = d as WeldingCanvas; canvas.MainRcp = e.NewValue as MainRecipe; canvas.UpdateCanCap(); } public static readonly DependencyProperty IsFlipYProperty = DependencyProperty.Register( nameof(IsFlipY), typeof(bool), typeof(WeldingCanvas), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (d, e) => (d as WeldingCanvas).OnFlipChanged(e) )); private void OnFlipChanged(object e) { InvalidateVisual(); } public bool IsFlipY { get { return (bool)GetValue(IsFlipYProperty); } set { SetValue(IsFlipYProperty, value); } } #endregion #region Canvas 내에 간단하게 사용할 Cell 정보 public PalletInfo PalletInfo { get => (PalletInfo)GetValue(PalletInfoProperty); set => SetValue(PalletInfoProperty, value); } public static readonly DependencyProperty PalletInfoProperty = DependencyProperty.Register(nameof(PalletInfo), typeof(PalletInfo), typeof(WeldingCanvas), new PropertyMetadata(null, OnPalletInfoChanged)); private static void OnPalletInfoChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var c = (WeldingCanvas)d; if (e.OldValue is PalletInfo oldInfo) { oldInfo.PalletStateChanged -= c.OnCellStateChanged; if (oldInfo.weldingData.HeightMeasurements != null) oldInfo.weldingData.HeightMeasurements.CollectionChanged -= c.HeightMeasurements_CollectionChanged; } if (e.NewValue is PalletInfo newInfo) { newInfo.PalletStateChanged += c.OnCellStateChanged; if (newInfo.weldingData.HeightMeasurements != null) { newInfo.weldingData.HeightMeasurements.CollectionChanged += c.HeightMeasurements_CollectionChanged; } } // 팔레트 교체 직후 화면도 갱신 c.InvalidateVisual(); } private void HeightMeasurements_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { Dispatcher.BeginInvoke(new Action(InvalidateVisual), DispatcherPriority.Render); } #endregion private void UpdateCanCap() { // 중앙 위치 CanCapCenterX = (MainRcp.Units[1].PosX + MainRcp.Units[2].PosX + MainRcp.Units[5].PosX + MainRcp.Units[6].PosX) / 4; CanCapCenterY = (MainRcp.Units[0].PosY + MainRcp.Units[3].PosY + MainRcp.Units[4].PosY + MainRcp.Units[7].PosY) / 4; // 중앙 위치 기준으로 위치 재배열 >> 실제 모터의 위치와 레이저가 시작되는 CanCap의 모터 위치가 다르기 때문 refLaserCanCapPosUnits = ReflectAcrossCenter(MainRcp.Units, (CanCapCenterX, CanCapCenterY)); // UI의 처음 위치로 이동시킴 TransformViewOriginPoints(ref refLaserCanCapPosUnits); InvalidateVisual(); } private void TransformViewOriginPoints(ref List points) { var minX = refLaserCanCapPosUnits.Min(p => p.X); var minY = refLaserCanCapPosUnits.Min(p => p.Y); for(int i = 0; i ReflectAcrossCenter(ObservableCollection units, (double X, double Y) center) { var ret = new List(); foreach (ProcessUnit unit in units) ret.Add(new Point(2 * center.X - unit.PosX, 2 * center.Y - unit.PosY)); return ret; } protected override void OnRender(DrawingContext dc) { base.OnRender(dc); canvasW = this.ActualWidth; canvasH = this.ActualHeight; // 1. 원본 도형 바운딩 박스 계산 rc.X = refLaserCanCapPosUnits.Min(p => p.X); rc.Y = refLaserCanCapPosUnits.Min(p => p.Y); rc.Width = refLaserCanCapPosUnits.Max(p => p.X) - rc.X; rc.Height = refLaserCanCapPosUnits.Max(p => p.Y) - rc.Y; //minX = refLaserCanCapPosUnits.Min(p => p.X); //maxX = refLaserCanCapPosUnits.Max(p => p.X); //minY = refLaserCanCapPosUnits.Min(p => p.Y); //maxY = refLaserCanCapPosUnits.Max(p => p.Y); // 도형 크기 //shapeW = maxX - minX; //shapeH = maxY - minY; // 2. 캔버스 내 최대 크기 확보 availableShapeW = canvasW - margin * 2; availableShapeH = canvasH - margin * 2; // 3. 비율 유지하면서 도형 확대 (가로/세로 중 작은 쪽 기준) scaleX = availableShapeW / rc.Width; // shapeW; scaleY = availableShapeH / rc.Height; // shapeH; shapeScale = Math.Min(scaleX, scaleY); scaledWidth = rc.Width * shapeScale; scaledHeight = rc.Height * shapeScale; // Test Source //refLaserCanCapPosUnits = new List //{ // new Point(150, 180), // new Point(170, 160), // new Point(170, 120), // new Point(150, 100), // new Point(30, 100), // new Point(10, 120), // new Point(10, 160), // new Point(30, 180), //}; DrawCanCap(dc); DrawTeachingPoint(dc); DrawNgOverlay(dc); DrawHeightMeas(dc); } private void DrawCanCap(DrawingContext dc) { if (refLaserCanCapPosUnits == null || refLaserCanCapPosUnits.Count != 8) return; // 5. Arc 반지름은 최종 그릴 크기 기준 비율로 설정 double arcRadius = Math.Min(scaledWidth, scaledHeight) * 0.3; // 6. Stage에 따라 Y축의 방향이 반대라.. 반대로 돌음 // X플립+Y플립(180도 회전)은 Arc 방향이 두 번 반전되어 원래대로 유지됨 var clockWise = IsFlipY ? SweepDirection.Counterclockwise : SweepDirection.Clockwise; // 7. 도형 정의 (시계방향) var geometry = new StreamGeometry(); using (var ctx = geometry.Open()) { ctx.BeginFigure(ToCanvas(refLaserCanCapPosUnits[0]), true, true); ctx.LineTo(ToCanvas(refLaserCanCapPosUnits[1]), true, false); ctx.ArcTo(ToCanvas(refLaserCanCapPosUnits[2]), new Size(arcRadius, arcRadius), 0, false, clockWise, true, false); ctx.LineTo(ToCanvas(refLaserCanCapPosUnits[3]), true, false); ctx.ArcTo(ToCanvas(refLaserCanCapPosUnits[4]), new Size(arcRadius, arcRadius), 0, false, clockWise, true, false); ctx.LineTo(ToCanvas(refLaserCanCapPosUnits[5]), true, false); ctx.ArcTo(ToCanvas(refLaserCanCapPosUnits[6]), new Size(arcRadius, arcRadius), 0, false, clockWise, true, false); ctx.LineTo(ToCanvas(refLaserCanCapPosUnits[7]), true, false); ctx.ArcTo(ToCanvas(refLaserCanCapPosUnits[0]), new Size(arcRadius, arcRadius), 0, false, clockWise, true, false); //ctx.LineTo(ToCanvas(refLaserCanCapPosUnits[0]), true, false); } geometry.Freeze(); dc.DrawGeometry(_brushCanCapLayout, _isWelding ? _penCanCaphightlightLayout : _penCanCapLayout, geometry); } private void DrawTeachingPoint(DrawingContext dc) { if (refLaserCanCapPosUnits == null || refLaserCanCapPosUnits.Count == 0) return; var typeface = new Typeface("Consolas"); // 폰트 설정 var fontSize = 14; // 폰트 크기 var radius = 10; // 원형 반지름 var Hmargin = 12.0; // 가로 마진 var Vmargin = 12.0; // 세로 마진 double centerX = refLaserCanCapPosUnits.Average(p => ToCanvas(p).X); double centerY = refLaserCanCapPosUnits.Average(p => ToCanvas(p).Y); var center = new Point(centerX, centerY); for (int i = 0; i < refLaserCanCapPosUnits.Count; i++) { var canvasPoint = ToCanvas(refLaserCanCapPosUnits[i]); // 바깥쪽으로 margin만큼 이동 Point outerPoint = canvasPoint; if(i == 2 || i == 3 || i == 6 || i == 7) outerPoint.X += (canvasPoint.X < center.X) ? -Hmargin : Hmargin; else outerPoint.Y += (canvasPoint.Y < center.Y) ? -Vmargin : Vmargin; // 원형 테두리 그리기 dc.DrawEllipse(null, _penCanCapLayout, outerPoint, radius, radius); // 숫자 텍스트 그리기 var text = new FormattedText( (i + 1).ToString(), System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, fontSize, Brushes.Black, VisualTreeHelper.GetDpi(this).PixelsPerDip); // 텍스트 중앙 정렬 (원 중심에 숫자 오도록) Point textLocation = new Point( outerPoint.X - text.Width / 2, outerPoint.Y - text.Height / 2); dc.DrawText(text, textLocation); } } private void DrawNgOverlay(DrawingContext dc) { // 조건 체크 if ((PalletInfo?.weldingData?.NgJudge ?? 0) <= 0) return; // 도형의 모델 좌표 중심 → 캔버스 좌표 중심으로 변환 var centerModel = new Point(rc.X + rc.Width / 2.0, rc.Y + rc.Height / 2.0); var center = ToCanvas(centerModel); // 도형 크기에 비례한 글자 크기 (너무 작지 않게 최소값 보장) double fontSize = Math.Max(40, Math.Min(scaledWidth, scaledHeight) * 0.18); var typeface = new Typeface( new FontFamily("Segoe UI"), FontStyles.Normal, FontWeights.Bold, FontStretches.Normal); var text = new FormattedText( $"{PalletInfo?.weldingData?.NgReason} NG", System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, fontSize, Brushes.Red, VisualTreeHelper.GetDpi(this).PixelsPerDip ); // 텍스트 드로우 var textPos = new Point(center.X - text.Width / 2, center.Y - text.Height / 2); dc.DrawText(text, textPos); } private void DrawHeightMeas(DrawingContext dc) { var heights = PalletInfo?.weldingData?.HeightMeasurements; if (heights == null || heights.Count == 0) return; if (refLaserCanCapPosUnits == null || refLaserCanCapPosUnits.Count != 8) return; if (rc.Width <= 0 || rc.Height <= 0) return; var typeface = new Typeface("Segoe UI"); double fontSize = Math.Max(12, Math.Min(scaledWidth, scaledHeight) * 0.06); const double labelH = 20; const double labelW = 44; // 장축 0→1 (상단), 4→5 (하단)의 캔버스 좌표 var p0 = ToCanvas(refLaserCanCapPosUnits[0]); var p1 = ToCanvas(refLaserCanCapPosUnits[1]); var p4 = ToCanvas(refLaserCanCapPosUnits[4]); var p5 = ToCanvas(refLaserCanCapPosUnits[5]); // Y값은 장축 끝점의 Y 그대로 사용 (별도 offset 없음) double topY = (p0.Y + p1.Y) / 2; double botY = (p4.Y + p5.Y) / 2; // X 범위 (ToCanvas에서 X 반전될 수 있으므로 min/max 사용) double topXMin = Math.Min(p0.X, p1.X); double topXMax = Math.Max(p0.X, p1.X); double botXMin = Math.Min(p4.X, p5.X); double botXMax = Math.Max(p4.X, p5.X); // 고정 인덱스 배치 (h1=heights[0] .. h8=heights[7]) // topY = pt[0]/pt[1] canvas Y, botY = pt[4]/pt[5] canvas Y // IsFlipY=True(StageA) : topY가 화면 아래, botY가 화면 위 → 시각 상단=h1,h2,h7,h8 / 하단=h3,h4,h5,h6 // IsFlipY=False(StageB): topY가 화면 위, botY가 화면 아래 → 시각 상단=h3,h4,h5,h6 / 하단=h1,h2,h7,h8 // 시각적 상하 반전은 ToCanvas()가 처리하므로 인덱스는 IsFlipY 무관하게 고정 int[] topIdx = new[] { 2, 3, 4, 5 }; // h3,h4,h5,h6 → topY(pt[0]/pt[1]) 위치에 배치 int[] botIdx = new[] { 0, 1, 6, 7 }; // h1,h2,h7,h8 → botY(pt[4]/pt[5]) 위치에 배치 DrawLongAxisLabels(dc, topIdx, heights, topXMin, topXMax, topY, labelW, labelH, typeface, fontSize); DrawLongAxisLabels(dc, botIdx, heights, botXMin, botXMax, botY, labelW, labelH, typeface, fontSize); } private void DrawLongAxisLabels(DrawingContext dc, int[] indices, ObservableCollection heights, double xMin, double xMax, double y, double labelW, double labelH, Typeface typeface, double fontSize) { const double iconH = 18; // body(10) + probe(8) const double iconGap = 3; int n = indices.Length; for (int k = 0; k < n; k++) { int idx = indices[k]; if (idx >= heights.Count) continue; double[] xRatios = { 0.08, 0.2, 0.8, 0.92 }; double x = xMin + (xMax - xMin) * (k < xRatios.Length ? xRatios[k] : (k + 1.0) / (n + 1)); // 아이콘: 레이블 바깥쪽(CanCap 반대 방향)에 배치 double iconCenterY = y - labelH / 2 - iconGap - iconH / 2; DrawSensorIcon(dc, new Point(x, iconCenterY)); DrawLabel(dc, new Point(x - labelW / 2, y - labelH / 2), $"{heights[idx]:0.###}", Brushes.Blue, typeface, fontSize); } } // 높이 측정 센서 아이콘 (몸체: 기울어진 사각형 + 탐침: 작은 사각형) // center : 아이콘 전체 중심 // probeDown : true → 탐침이 아래(CanCap 방향), false → 탐침이 위 private void DrawSensorIcon(DrawingContext dc, Point center) { var fill = new SolidColorBrush(Color.FromRgb(40, 110, 40)); // 진한 녹색 const double bw = 20; // 몸체 너비 const double bh = 10; // 몸체 높이 const double lean = 4; // 몸체 기울기 (parallelogram offset) const double pw = 6; // 탐침 너비 const double ph = 8; // 탐침 높이 double top = center.Y - (bh + ph) / 2.0; double bodyY1, bodyY2, probeY1; bodyY1 = top; bodyY2 = top + bh; probeY1 = bodyY2; Point[] pts = new[] { new Point(center.X - bw / 2, bodyY1), // 상단 좌 new Point(center.X + bw / 2, bodyY1), // 상단 우 new Point(center.X + bw / 2 - lean, bodyY2), // 하단 우 new Point(center.X - bw / 2 - lean, bodyY2), // 하단 좌 (왼쪽으로 더 튀어남) }; var geo = new StreamGeometry(); using (var ctx = geo.Open()) { ctx.BeginFigure(pts[0], true, true); ctx.LineTo(pts[1], true, false); ctx.LineTo(pts[2], true, false); ctx.LineTo(pts[3], true, false); } geo.Freeze(); dc.DrawGeometry(fill, null, geo); // 탐침: 중앙 정렬된 작은 사각형 dc.DrawRectangle(fill, null, new Rect(center.X - pw / 2, probeY1, pw, ph)); } private void DrawLabel(DrawingContext dc, Point pos, string text, Brush txtcollor, Typeface typeface, double fontSize) { var ft = new FormattedText(text, System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, typeface, fontSize, txtcollor, VisualTreeHelper.GetDpi(this).PixelsPerDip); var padding = new Thickness(6, 2, 6, 2); var rect = new Rect(pos.X, pos.Y, ft.Width + padding.Left + padding.Right, ft.Height + padding.Top + padding.Bottom); dc.DrawRoundedRectangle( new SolidColorBrush(Color.FromArgb(180, 255, 255, 255)), new Pen(Brushes.Black, 1), rect, 4, 4); dc.DrawText(ft, new Point(pos.X + padding.Left, pos.Y + padding.Top)); } // 6. 변환 함수: 수학 좌표계 → WPF 좌표계 // 수학 좌표계 → WPF 좌표계 변환 // 작업대 방향이 180도 반전됨에 따라 X/Y 모두 반전 적용 public Point ToCanvas(Point pt) { // 도형 중앙 정렬 double offsetX = (canvasW - scaledWidth) / 2; double offsetY = (canvasH - scaledHeight) / 2; double xBase = (pt.X - rc.X) * shapeScale + offsetX; double yBase = (pt.Y - rc.Y) * shapeScale + offsetY; // X축 반전 (작업대 180도 회전) double x = canvasW - xBase; // Y축: IsFlipY 로직 반전 (작업대 180도 회전) double y = IsFlipY ? (canvasH - yBase) : yBase; return new Point(x, y); } protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { base.OnRenderSizeChanged(sizeInfo); } } }