643 lines
26 KiB
C#
643 lines
26 KiB
C#
using Prism.Ioc;
|
||
using Serilog;
|
||
using System;
|
||
using System.ComponentModel;
|
||
using System.Windows;
|
||
using System.Windows.Controls;
|
||
using System.Windows.Input;
|
||
using System.Windows.Media;
|
||
using System.Windows.Media.Imaging;
|
||
using XplorePlane.Services.Hardware.Camera;
|
||
using XplorePlane.Services.Hardware.Coordinate;
|
||
using XplorePlane.Views.Main.Overlays;
|
||
|
||
namespace XplorePlane.Views.Main
|
||
{
|
||
public partial class NavigationPropertyPanelView : UserControl
|
||
{
|
||
private static readonly ILogger _diag = Log.ForContext<NavigationPropertyPanelView>();
|
||
private NavigationPropertyPanelViewModel? _viewModel;
|
||
private InspectionMarkOverlay? _navMarkOverlay;
|
||
private IViewCoordinateMapper? _mapper;
|
||
|
||
// —— 青框画中画状态 ——
|
||
/// <summary>从原图裁切的正方形边长(像素),固定不变</summary>
|
||
private double _roiSizePx = 60;
|
||
/// <summary>屏幕上青色框的显示边长(像素坐标系),随滚轮变化</summary>
|
||
private double _roiScreenPx = 150;
|
||
/// <summary>当前 ROI 中心(图像像素坐标系)——裁切区域中心</summary>
|
||
private Point _roiCenterPx;
|
||
/// <summary>青框是否可见</summary>
|
||
private bool _pipVisible;
|
||
/// <summary>是否处于放大锁定模式(框固定,鼠标在框内精确移动)</summary>
|
||
private bool _isZoomedLocked;
|
||
/// <summary>放大锁定时,框中心在图像像素坐标系中的固定位置</summary>
|
||
private Point _lockedFrameCenter;
|
||
/// <summary>放大锁定时,青色十字线对应的精确图像像素位置</summary>
|
||
private Point _preciseTargetPx;
|
||
|
||
// 缩放参数
|
||
private const double PipScaleFactor = 1.2;
|
||
private const double PipScreenMin = 80;
|
||
private const double PipScreenMax = 800;
|
||
/// <summary>未放大时的基础框大小(等于 roiSizePx 时为 1:1)</summary>
|
||
private const double PipScreenBase = 150;
|
||
|
||
public NavigationPropertyPanelView()
|
||
{
|
||
InitializeComponent();
|
||
ResolveViewModel();
|
||
Loaded += OnLoaded;
|
||
}
|
||
|
||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||
{
|
||
ResolveViewModel();
|
||
AttachMarkOverlay();
|
||
|
||
// 将导航窗体实际渲染结果注册给 CNC 归档服务,包含标记叠加层。
|
||
try
|
||
{
|
||
var frameProvider = AppBootstrapper.Instance?.Container.Resolve<INavigationCameraFrameProvider>();
|
||
frameProvider?.SetCompositeCapture(CaptureCompositeImage);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_diag.Warning(ex, "注册导航合成截图回调失败");
|
||
}
|
||
|
||
// 订阅 ViewModel 属性变化,驱动红十字线位置更新
|
||
if (_viewModel != null)
|
||
{
|
||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||
UpdateCrosshair();
|
||
|
||
// 注入合成截图委托:保存导航图时对相机背景 + 标记叠加层一并截图
|
||
_viewModel.CompositeImageProvider = CaptureCompositeImage;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将导航图像显示区域(相机背景 + 标记叠加层)渲染为合成位图,
|
||
/// 供"保存导航图照片"对用户实际看到的图层进行截图。
|
||
/// </summary>
|
||
private BitmapSource? CaptureCompositeImage()
|
||
{
|
||
if (!Dispatcher.CheckAccess())
|
||
return Dispatcher.Invoke(CaptureCompositeImage);
|
||
|
||
var target = cameraDisplayHost;
|
||
if (target == null)
|
||
return null;
|
||
|
||
int width = (int)target.ActualWidth;
|
||
int height = (int)target.ActualHeight;
|
||
if (width <= 0 || height <= 0)
|
||
return null;
|
||
|
||
try
|
||
{
|
||
var dpi = VisualTreeHelper.GetDpi(target);
|
||
var rtb = new RenderTargetBitmap(
|
||
(int)(width * dpi.DpiScaleX),
|
||
(int)(height * dpi.DpiScaleY),
|
||
dpi.PixelsPerInchX,
|
||
dpi.PixelsPerInchY,
|
||
PixelFormats.Pbgra32);
|
||
rtb.Render(target);
|
||
rtb.Freeze();
|
||
return rtb;
|
||
}
|
||
catch
|
||
{
|
||
return null;
|
||
}
|
||
}
|
||
|
||
private void ResolveViewModel()
|
||
{
|
||
if (DataContext is NavigationPropertyPanelViewModel viewModel)
|
||
{
|
||
_viewModel = viewModel;
|
||
return;
|
||
}
|
||
|
||
var bootstrapper = AppBootstrapper.Instance;
|
||
if (bootstrapper == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_viewModel = bootstrapper.Container.Resolve<NavigationPropertyPanelViewModel>();
|
||
DataContext = _viewModel;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 在导航相机图像上挂载只读标记叠加层,与主视口共享同一份 Marks 集合(单例 VM),
|
||
/// 通过坐标映射服务把探测器小视野坐标换算到导航大视野位置,实现双视野标记同步显示。
|
||
/// </summary>
|
||
private void AttachMarkOverlay()
|
||
{
|
||
if (_navMarkOverlay != null)
|
||
return;
|
||
|
||
var doc = _viewModel?.InspectionDocumentation;
|
||
if (doc == null)
|
||
{
|
||
_diag.Warning("[NavMarkDiag] AttachMarkOverlay 中止: InspectionDocumentation 为 null (viewModel 存在={HasVm})", _viewModel != null);
|
||
return;
|
||
}
|
||
|
||
try { _mapper = AppBootstrapper.Instance?.Container.Resolve<IViewCoordinateMapper>(); }
|
||
catch (Exception ex) { _mapper = null; _diag.Warning(ex, "[NavMarkDiag] IViewCoordinateMapper 解析异常"); }
|
||
if (_mapper == null)
|
||
{
|
||
_diag.Warning("[NavMarkDiag] AttachMarkOverlay 中止: IViewCoordinateMapper 为 null");
|
||
return;
|
||
}
|
||
|
||
var overlay = new InspectionMarkOverlay
|
||
{
|
||
IsReadOnly = true,
|
||
IsHitTestVisible = false,
|
||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||
VerticalAlignment = VerticalAlignment.Stretch,
|
||
Background = Brushes.Transparent,
|
||
ClipToBounds = false,
|
||
ShowMarks = doc.ShowMarks
|
||
};
|
||
Panel.SetZIndex(overlay, 30000);
|
||
|
||
// 大视野坐标解析:探测器像素 → 导航图归一化 → 导航图像素坐标 → 外层显示坐标。
|
||
// overlay 挂在 Viewbox 外层,图标保持屏幕尺寸,不会随导航原图一起缩小。
|
||
overlay.CoordinateResolver = mark =>
|
||
{
|
||
var (w, h) = GetNavImagePixelSize();
|
||
if (w <= 0 || h <= 0)
|
||
{
|
||
_diag.Warning("[NavMarkDiag] Resolver 返回null: 导航图尺寸无效 (w={W}, h={H})", w, h);
|
||
return null;
|
||
}
|
||
|
||
var map = doc.CurrentMap;
|
||
var context = new ViewMappingContext
|
||
{
|
||
DetectorImageWidth = map?.ImageWidth ?? 0,
|
||
DetectorImageHeight = map?.ImageHeight ?? 0,
|
||
NavigationImageWidth = w,
|
||
NavigationImageHeight = h,
|
||
Motion = mark.StateSnapshot?.Motion ?? MotionState.Default
|
||
};
|
||
|
||
var normalized = _mapper.DetectorPixelToNavigationNormalized(new Point(mark.PixelX, mark.PixelY), context);
|
||
if (normalized == null)
|
||
{
|
||
_diag.Warning("[NavMarkDiag] Resolver 返回null: mapper无法映射 (DetectorImageWidth={DW}, CurrentMap存在={HasMap}, pixel=({PX},{PY}))", context.DetectorImageWidth, map != null, mark.PixelX, mark.PixelY);
|
||
return null;
|
||
}
|
||
|
||
var navPixel = new Point(normalized.Value.X * w, normalized.Value.Y * h);
|
||
var displayPos = MapNavigationPixelToDisplay(navPixel, w, h);
|
||
if (displayPos == null)
|
||
{
|
||
_diag.Warning("[NavMarkDiag] Resolver 返回null: 显示区域尺寸无效 (hostW={HostW:F0}, hostH={HostH:F0}, navW={W:F0}, navH={H:F0})",
|
||
cameraDisplayHost.ActualWidth, cameraDisplayHost.ActualHeight, w, h);
|
||
return null;
|
||
}
|
||
|
||
_diag.Information("[NavMarkDiag] Resolver ok: pixel=({PX},{PY}) -> nav=({NX:F0},{NY:F0}) -> display=({X:F0},{Y:F0}) [navW={W:F0},navH={H:F0}]",
|
||
mark.PixelX, mark.PixelY, navPixel.X, navPixel.Y, displayPos.Value.X, displayPos.Value.Y, w, h);
|
||
return displayPos;
|
||
};
|
||
|
||
overlay.Marks = doc.Marks;
|
||
|
||
// 显示/隐藏标记随主控 VM 同步
|
||
doc.PropertyChanged += (_, ev) =>
|
||
{
|
||
if (_navMarkOverlay == null)
|
||
return;
|
||
if (ev.PropertyName == nameof(doc.ShowMarks))
|
||
_navMarkOverlay.ShowMarks = doc.ShowMarks;
|
||
};
|
||
|
||
cameraDisplayHost.Children.Add(overlay);
|
||
_navMarkOverlay = overlay;
|
||
cameraDisplayHost.SizeChanged += (_, _) => SyncMarkOverlaySize();
|
||
_diag.Information("[NavMarkDiag] overlay 已挂载, 当前 Marks.Count={Count}, CurrentMap存在={HasMap}, ShowMarks={Show}", doc.Marks.Count, doc.CurrentMap != null, doc.ShowMarks);
|
||
|
||
// 诊断:监听 Marks 集合变化,确认主视口添加的标记是否同步到导航区(共享单例集合)
|
||
doc.Marks.CollectionChanged += (_, ev) =>
|
||
_diag.Information("[NavMarkDiag] 导航区收到 Marks 变化: Action={Action}, Count={Count}", ev.Action, doc.Marks.Count);
|
||
|
||
// 外层显示区域尺寸变化时,需要按新的 Uniform 缩放比例重绘标记。
|
||
SyncMarkOverlaySize();
|
||
}
|
||
|
||
private Point? MapNavigationPixelToDisplay(Point navPixel, double navWidth, double navHeight)
|
||
{
|
||
if (navWidth <= 0 || navHeight <= 0)
|
||
return null;
|
||
|
||
double hostWidth = cameraDisplayHost.ActualWidth;
|
||
double hostHeight = cameraDisplayHost.ActualHeight;
|
||
if (hostWidth <= 0 || hostHeight <= 0)
|
||
return null;
|
||
|
||
double scale = Math.Min(hostWidth / navWidth, hostHeight / navHeight);
|
||
if (scale <= 0 || double.IsNaN(scale) || double.IsInfinity(scale))
|
||
return null;
|
||
|
||
double displayedWidth = navWidth * scale;
|
||
double displayedHeight = navHeight * scale;
|
||
double offsetX = (hostWidth - displayedWidth) / 2.0;
|
||
double offsetY = (hostHeight - displayedHeight) / 2.0;
|
||
|
||
return new Point(offsetX + navPixel.X * scale, offsetY + navPixel.Y * scale);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导航相机图像的像素尺寸;标记叠加层与红十字线共用此坐标系。
|
||
/// 优先取 ViewModel.ImageWidth/Height(拍快照时随图像一并设置),
|
||
/// 回退到 CameraImageSource 的像素尺寸。
|
||
/// </summary>
|
||
private (double Width, double Height) GetNavImagePixelSize()
|
||
{
|
||
double w = _viewModel?.ImageWidth ?? 0;
|
||
double h = _viewModel?.ImageHeight ?? 0;
|
||
if (w <= 0 || h <= 0)
|
||
{
|
||
var src = _viewModel?.CameraImageSource;
|
||
if (src != null)
|
||
{
|
||
w = src.PixelWidth;
|
||
h = src.PixelHeight;
|
||
}
|
||
}
|
||
return (w, h);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按当前外层显示区域尺寸重绘标记。
|
||
/// 标记位置先在导航图像像素坐标中计算,再映射到 Viewbox 外层的显示坐标;
|
||
/// 图标自身保持固定屏幕尺寸,避免被 Viewbox 缩成不可见的小点。
|
||
/// </summary>
|
||
private void SyncMarkOverlaySize()
|
||
{
|
||
if (_navMarkOverlay == null)
|
||
return;
|
||
|
||
// 标记坐标由 CoordinateResolver 按当前图像尺寸和显示区域尺寸实时计算,这里仅触发重绘。
|
||
_navMarkOverlay.RefreshMarks();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 监听 ViewModel 属性变化,当红十字线相关属性更新时刷新绘制。
|
||
/// </summary>
|
||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||
{
|
||
switch (e.PropertyName)
|
||
{
|
||
case nameof(NavigationPropertyPanelViewModel.CrosshairPixelX):
|
||
case nameof(NavigationPropertyPanelViewModel.CrosshairPixelY):
|
||
case nameof(NavigationPropertyPanelViewModel.IsCrosshairVisible):
|
||
case nameof(NavigationPropertyPanelViewModel.CameraImageSource):
|
||
UpdateCrosshair();
|
||
SyncMarkOverlaySize();
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新红色十字线的位置和可见性。
|
||
/// 十字线绘制在 Canvas 上,Canvas 与 Image 重叠在同一 Grid 中,
|
||
/// 由 Viewbox 统一缩放。因此十字线坐标直接使用图像像素坐标即可。
|
||
/// /// 十字线含义:
|
||
/// - 中心点 = 探测器中心在导航图上的当前投影位置
|
||
/// - 载物台移动 → 编码器变化 → WorldToPixel 反算 → 十字线移动
|
||
/// </summary>
|
||
private void UpdateCrosshair()
|
||
{
|
||
if (_viewModel == null) return;
|
||
|
||
bool visible = _viewModel.IsCrosshairVisible && _viewModel.HasSnapshot;
|
||
|
||
crosshairCanvas.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
|
||
|
||
if (!visible) return;
|
||
|
||
// 红十字线位置(图像像素坐标系)
|
||
double cx = _viewModel.CrosshairPixelX;
|
||
double cy = _viewModel.CrosshairPixelY;
|
||
// 获取图像尺寸用于设置 Canvas 大小和线条端点
|
||
double imgW = _viewModel.ImageWidth;
|
||
double imgH = _viewModel.ImageHeight;
|
||
|
||
if (imgW <= 0 || imgH <= 0) return;
|
||
|
||
// 设置 Canvas 尺寸与图像一致(Viewbox 内部按像素坐标工作)
|
||
crosshairCanvas.Width = imgW;
|
||
crosshairCanvas.Height = imgH;
|
||
|
||
// 垂直线:从图像顶部到底部,X 固定在 cx
|
||
// 垂直线
|
||
crosshairVertical.X1 = cx;
|
||
crosshairVertical.Y1 = 0;
|
||
crosshairVertical.X2 = cx;
|
||
crosshairVertical.Y2 = imgH;
|
||
|
||
// 水平线
|
||
crosshairHorizontal.X1 = 0;
|
||
crosshairHorizontal.Y1 = cy;
|
||
crosshairHorizontal.X2 = imgW;
|
||
crosshairHorizontal.Y2 = cy;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导航下拉按钮点击:在按钮正下方打开 ContextMenu
|
||
/// </summary>
|
||
private void NavDropDownButton_Click(object sender, RoutedEventArgs e)
|
||
{
|
||
if (sender is Button button && button.ContextMenu != null)
|
||
{
|
||
button.ContextMenu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
|
||
button.ContextMenu.PlacementTarget = button;
|
||
button.ContextMenu.IsOpen = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单击导航图像:将点击像素对位到探测器中心。
|
||
/// - 跟随模式:使用点击位置
|
||
/// - 放大锁定模式:使用框内精确目标位置
|
||
/// </summary>
|
||
private void ImgCamera_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||
{
|
||
if (_viewModel == null) return;
|
||
if (_viewModel.CameraImageSource == null) return;
|
||
if (!_viewModel.IsCalibrated || !_viewModel.HasSnapshot) return;
|
||
if (_viewModel.IsMoving) return;
|
||
|
||
double px, py;
|
||
|
||
if (_isZoomedLocked)
|
||
{
|
||
// 放大锁定模式:使用精确目标位置
|
||
px = _preciseTargetPx.X;
|
||
py = _preciseTargetPx.Y;
|
||
}
|
||
else
|
||
{
|
||
// 跟随模式:使用点击位置换算
|
||
var image = (Image)sender;
|
||
var pos = e.GetPosition(image);
|
||
px = pos.X / image.ActualWidth * _viewModel.CameraImageSource.PixelWidth;
|
||
py = pos.Y / image.ActualHeight * _viewModel.CameraImageSource.PixelHeight;
|
||
}
|
||
|
||
// 边界检查
|
||
if (px < 0 || px >= _viewModel.CameraImageSource.PixelWidth ||
|
||
py < 0 || py >= _viewModel.CameraImageSource.PixelHeight)
|
||
{
|
||
return;
|
||
}
|
||
|
||
_ = _viewModel.NavigateToPixel(px, py);
|
||
}
|
||
|
||
#region PiP (Picture-in-Picture) - 青框画中画
|
||
|
||
/// <summary>
|
||
/// 鼠标进入图像区域:显示青框。
|
||
/// </summary>
|
||
private void ImageContainer_MouseEnter(object sender, MouseEventArgs e)
|
||
{
|
||
if (_viewModel == null || !_viewModel.HasSnapshot) return;
|
||
_pipVisible = true;
|
||
cyanCrosshairCanvas.Visibility = Visibility.Visible;
|
||
pipCanvas.Visibility = Visibility.Visible;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 鼠标离开图像区域:隐藏青框,退出锁定模式。
|
||
/// </summary>
|
||
private void ImageContainer_MouseLeave(object sender, MouseEventArgs e)
|
||
{
|
||
_pipVisible = false;
|
||
_isZoomedLocked = false;
|
||
_roiScreenPx = PipScreenBase;
|
||
cyanCrosshairCanvas.Visibility = Visibility.Collapsed;
|
||
pipCanvas.Visibility = Visibility.Collapsed;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 鼠标移动:
|
||
/// - 未放大锁定:框跟随鼠标(粗定位模式)
|
||
/// - 放大锁定:鼠标在框内移动映射到精确图像像素位置(精确定位模式)
|
||
/// 鼠标移出框区域时自动退出锁定,恢复跟随模式
|
||
/// </summary>
|
||
private void ImageContainer_MouseMove(object sender, MouseEventArgs e)
|
||
{
|
||
if (_viewModel == null || !_viewModel.HasSnapshot) return;
|
||
if (_viewModel.CameraImageSource == null) return;
|
||
|
||
var pos = e.GetPosition(imageContainer);
|
||
double imgW = _viewModel.ImageWidth;
|
||
double imgH = _viewModel.ImageHeight;
|
||
|
||
if (imgW <= 0 || imgH <= 0) return;
|
||
|
||
if (!_pipVisible) return;
|
||
|
||
if (_isZoomedLocked)
|
||
{
|
||
// 检查鼠标是否还在框区域内
|
||
double halfScreen = _roiScreenPx / 2.0;
|
||
double frameLeft = _lockedFrameCenter.X - halfScreen;
|
||
double frameTop = _lockedFrameCenter.Y - halfScreen;
|
||
double frameRight = _lockedFrameCenter.X + halfScreen;
|
||
double frameBottom = _lockedFrameCenter.Y + halfScreen;
|
||
|
||
if (pos.X < frameLeft || pos.X > frameRight ||
|
||
pos.Y < frameTop || pos.Y > frameBottom)
|
||
{
|
||
// 鼠标移出框区域 → 退出锁定,恢复初始状态
|
||
_isZoomedLocked = false;
|
||
_roiScreenPx = PipScreenBase;
|
||
}
|
||
}
|
||
|
||
if (_isZoomedLocked)
|
||
{
|
||
// ═══ 放大锁定模式 ═══
|
||
// 鼠标相对于框中心的偏移 → 映射为图像像素偏移
|
||
double magnification = _roiScreenPx / _roiSizePx;
|
||
double mouseOffsetX = pos.X - _lockedFrameCenter.X;
|
||
double mouseOffsetY = pos.Y - _lockedFrameCenter.Y;
|
||
|
||
// 鼠标在框内偏移 / 放大倍率 = 图像像素偏移
|
||
double imgOffsetX = mouseOffsetX / magnification;
|
||
double imgOffsetY = mouseOffsetY / magnification;
|
||
|
||
// 精确目标位置 = ROI中心 + 偏移(clamp 到裁切区域内)
|
||
double halfRoi = _roiSizePx / 2.0;
|
||
double targetX = _roiCenterPx.X + imgOffsetX;
|
||
double targetY = _roiCenterPx.Y + imgOffsetY;
|
||
targetX = Math.Max(_roiCenterPx.X - halfRoi, Math.Min(targetX, _roiCenterPx.X + halfRoi));
|
||
targetY = Math.Max(_roiCenterPx.Y - halfRoi, Math.Min(targetY, _roiCenterPx.Y + halfRoi));
|
||
targetX = Math.Max(0, Math.Min(targetX, imgW));
|
||
targetY = Math.Max(0, Math.Min(targetY, imgH));
|
||
|
||
_preciseTargetPx = new Point(targetX, targetY);
|
||
|
||
// 更新青色十字线显示在底图上对应的精确位置
|
||
UpdateCyanCrosshair(targetX, targetY, imgW, imgH);
|
||
// 画中画位置不变,只更新框内的十字指示(通过青色十字线体现)
|
||
UpdatePip(_roiCenterPx.X, _roiCenterPx.Y, imgW, imgH);
|
||
}
|
||
else
|
||
{
|
||
// ═══ 跟随模式(粗定位)═══
|
||
double cx = Math.Max(0, Math.Min(pos.X, imgW));
|
||
double cy = Math.Max(0, Math.Min(pos.Y, imgH));
|
||
_roiCenterPx = new Point(cx, cy);
|
||
_preciseTargetPx = new Point(cx, cy);
|
||
|
||
UpdateCyanCrosshair(cx, cy, imgW, imgH);
|
||
UpdatePip(cx, cy, imgW, imgH);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 滚轮:缩放青色框大小。
|
||
/// - 第一次放大时进入锁定模式(框中心固定)
|
||
/// - 缩回最小时退出锁定模式(恢复跟随)
|
||
/// </summary>
|
||
private void ImageContainer_MouseWheel(object sender, MouseWheelEventArgs e)
|
||
{
|
||
if (_viewModel == null || !_viewModel.HasSnapshot) return;
|
||
if (!_pipVisible) return;
|
||
|
||
double oldSize = _roiScreenPx;
|
||
|
||
if (e.Delta > 0)
|
||
{
|
||
// 滚轮向上 → 框变大 → 放大显示
|
||
_roiScreenPx = Math.Min(_roiScreenPx * PipScaleFactor, PipScreenMax);
|
||
|
||
// 首次放大时进入锁定模式
|
||
if (!_isZoomedLocked && _roiScreenPx > PipScreenBase)
|
||
{
|
||
_isZoomedLocked = true;
|
||
_lockedFrameCenter = _roiCenterPx; // 锁定框中心为当前 ROI 中心的屏幕位置
|
||
// lockedFrameCenter 在 Viewbox 内就是图像像素坐标
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 滚轮向下 → 框变小 → 缩小显示
|
||
_roiScreenPx = Math.Max(_roiScreenPx / PipScaleFactor, PipScreenMin);
|
||
|
||
// 缩回基础大小时退出锁定模式
|
||
if (_roiScreenPx <= PipScreenBase)
|
||
{
|
||
_roiScreenPx = PipScreenBase;
|
||
_isZoomedLocked = false;
|
||
}
|
||
}
|
||
|
||
// 刷新画中画
|
||
double imgW = _viewModel.ImageWidth;
|
||
double imgH = _viewModel.ImageHeight;
|
||
if (imgW > 0 && imgH > 0)
|
||
{
|
||
UpdatePip(_roiCenterPx.X, _roiCenterPx.Y, imgW, imgH);
|
||
}
|
||
|
||
e.Handled = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新青色十字线位置(全图横竖线,标示当前精确关注点)。
|
||
/// </summary>
|
||
private void UpdateCyanCrosshair(double cx, double cy, double imgW, double imgH)
|
||
{
|
||
cyanCrosshairCanvas.Width = imgW;
|
||
cyanCrosshairCanvas.Height = imgH;
|
||
|
||
cyanCrosshairVertical.X1 = cx;
|
||
cyanCrosshairVertical.Y1 = 0;
|
||
cyanCrosshairVertical.X2 = cx;
|
||
cyanCrosshairVertical.Y2 = imgH;
|
||
|
||
cyanCrosshairHorizontal.X1 = 0;
|
||
cyanCrosshairHorizontal.Y1 = cy;
|
||
cyanCrosshairHorizontal.X2 = imgW;
|
||
cyanCrosshairHorizontal.Y2 = cy;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新画中画内容和位置。
|
||
///
|
||
/// 逻辑:
|
||
/// 1. 以 roiCenterPx 为中心,裁切 roiSizePx × roiSizePx 的原图区域
|
||
/// 2. 将裁切结果显示在大小为 roiScreenPx × roiScreenPx 的青色框内
|
||
/// 3. 跟随模式:框中心在鼠标位置;锁定模式:框中心固定在 lockedFrameCenter
|
||
/// 4. 如果探测器中心(红十字)在裁切区域内,在框内绘制红色十字线
|
||
/// </summary>
|
||
private void UpdatePip(double cx, double cy, double imgW, double imgH)
|
||
{
|
||
var source = _viewModel?.CameraImageSource;
|
||
if (source == null) return;
|
||
|
||
int srcW = source.PixelWidth;
|
||
int srcH = source.PixelHeight;
|
||
|
||
double halfRoi = _roiSizePx / 2.0;
|
||
|
||
// 计算裁切区域(图像像素),与边界求交
|
||
int left = (int)Math.Max(0, cx - halfRoi);
|
||
int top = (int)Math.Max(0, cy - halfRoi);
|
||
int right = (int)Math.Min(srcW, cx + halfRoi);
|
||
int bottom = (int)Math.Min(srcH, cy + halfRoi);
|
||
|
||
int cropW = right - left;
|
||
int cropH = bottom - top;
|
||
|
||
if (cropW <= 0 || cropH <= 0) return;
|
||
|
||
// 裁切原图
|
||
try
|
||
{
|
||
var rect = new Int32Rect(left, top, cropW, cropH);
|
||
var cropped = new CroppedBitmap(source, rect);
|
||
pipImage.Source = cropped;
|
||
}
|
||
catch
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 设置框的显示大小
|
||
pipBorder.Width = _roiScreenPx;
|
||
pipBorder.Height = _roiScreenPx;
|
||
|
||
// 定位框中心
|
||
double frameCenterX = _isZoomedLocked ? _lockedFrameCenter.X : cx;
|
||
double frameCenterY = _isZoomedLocked ? _lockedFrameCenter.Y : cy;
|
||
|
||
double pipLeft = frameCenterX - _roiScreenPx / 2.0;
|
||
double pipTop = frameCenterY - _roiScreenPx / 2.0;
|
||
|
||
Canvas.SetLeft(pipBorder, pipLeft);
|
||
Canvas.SetTop(pipBorder, pipTop);
|
||
|
||
// 确保 pipCanvas 尺寸覆盖整个图像
|
||
pipCanvas.Width = imgW;
|
||
pipCanvas.Height = imgH;
|
||
}
|
||
|
||
#endregion PiP
|
||
}
|
||
}
|