540 lines
23 KiB
C#
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);
|
|
}
|
|
|
|
}
|
|
}
|