2026-06-05 13:44:39 +09:00

540 lines
23 KiB
C#

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<Point> 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<Point> points)
{
var minX = refLaserCanCapPosUnits.Min(p => p.X);
var minY = refLaserCanCapPosUnits.Min(p => p.Y);
for(int i = 0; i<refLaserCanCapPosUnits.Count; i++) {
points[i] = new Point(points[i].X - minX, points[i].Y - minY);
}
}
public List<Point> ReflectAcrossCenter(ObservableCollection<ProcessUnit> units, (double X, double Y) center)
{
var ret = new List<Point>();
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<Point>
//{
// 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<double> 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);
}
}
}