Files

3193 lines
142 KiB
C#
Raw Permalink Normal View History

2026-02-21 16:51:34 +08:00
using Ink_Canvas.Helpers;
2025-08-31 11:43:52 +08:00
using System;
2025-05-25 09:29:48 +08:00
using System.Collections.Generic;
2025-07-28 14:40:44 +08:00
using System.Diagnostics;
2025-05-25 09:29:48 +08:00
using System.Linq;
2026-03-28 18:40:18 +08:00
using System.Threading;
2025-07-26 19:03:07 +08:00
using System.Threading.Tasks;
2025-05-25 09:29:48 +08:00
using System.Windows;
using System.Windows.Controls;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
2025-09-30 19:16:56 +08:00
using System.Windows.Threading;
2025-05-25 09:29:48 +08:00
using Point = System.Windows.Point;
2025-08-03 16:46:33 +08:00
namespace Ink_Canvas
{
/// <summary>
/// 主窗口类的部分类,包含压感模拟和墨水到形状识别的功能
/// </summary>
/// <remarks>
/// 本文件主要包含以下功能:
/// 1. 压感模拟:根据输入设备类型和设置模拟不同的压感效果
/// 2. 墨水到形状识别:将手绘墨迹转换为规则形状(直线、圆形、椭圆、三角形、矩形等)
/// 3. 直线自动拉直:将近似直线的墨迹自动拉成直线
/// 4. 端点吸附:将直线端点吸附到其他直线的端点
/// 5. 矩形参考线系统:通过多条直线构成矩形
/// 6. 高级贝塞尔曲线平滑:对墨迹进行平滑处理
/// 7. 异步墨水处理:提高性能的异步墨水处理机制
/// </remarks>
2025-08-03 16:46:33 +08:00
public partial class MainWindow : Window
{
private Helpers.ModernInkAnalyzer _modernInkAnalyzer;
private Helpers.ModernInkAnalyzer ModernInkAnalyzer =>
_modernInkAnalyzer ??= new Helpers.ModernInkAnalyzer();
/// <summary>
/// 存储新的笔画集合,用于形状识别
/// </summary>
2025-05-25 09:29:48 +08:00
private StrokeCollection newStrokes = new StrokeCollection();
/// <summary>
/// 存储圆形形状的列表
/// </summary>
2025-05-25 09:29:48 +08:00
private List<Circle> circles = new List<Circle>();
2026-04-04 22:16:37 +08:00
/// <summary>串行执行墨迹转形状。WinRT 必须在 Dispatcher 上延后执行:若在 StrokeCollectedUI 线程)内对 WinRT 路径同步 GetResult()AnalyzeAsync 延续再贴回 UI 时会死锁。</summary>
2026-03-28 18:40:18 +08:00
private static readonly SemaphoreSlim InkToShapeSerial = new SemaphoreSlim(1, 1);
2026-03-29 12:24:13 +08:00
/// <summary>
/// 收笔后待参与「手写体字形替换」的最近笔迹(多笔一字/一词时由 WinRT InkAnalyzer 合并为 InkWord)。
/// </summary>
private readonly StrokeCollection _handwritingRecentStrokesForBeautify = new StrokeCollection();
private const int HandwritingBeautifyMaxRecentStrokes = 20;
2026-03-29 12:24:13 +08:00
/// <summary>手写体矫正:停笔后延迟执行(毫秒),多笔一字时等用户写完再识别。</summary>
private const int HandwritingBeautifyDebounceMs = 1000;
private DispatcherTimer _handwritingBeautifyDebounceTimer;
/// <summary>每次收笔并入批次时递增;防抖 Tick 携带快照,识别过程中若又有新笔则放弃本轮替换。</summary>
private ulong _handwritingBeautifyScheduleRevision;
2026-04-04 22:16:37 +08:00
/// <summary>画布笔画 → 手写纠正的识别输入(收笔时、笔锋/首段压感合成前的点集副本;替换墨迹时仍移除画布上的原笔画)。</summary>
private readonly Dictionary<Stroke, Stroke> _handwritingBeautifyInkInputByCanvasStroke =
new Dictionary<Stroke, Stroke>();
/// <summary>
/// 矩形参考线的列表
/// </summary>
2025-07-29 23:14:20 +08:00
private List<RectangleGuideLine> rectangleGuideLines = new List<RectangleGuideLine>();
/// <summary>
/// 矩形端点的阈值
/// </summary>
2025-12-13 20:32:43 +08:00
private const double RECTANGLE_ENDPOINT_THRESHOLD = 30.0;
/// <summary>
/// 矩形角度的阈值
/// </summary>
2025-12-13 20:32:43 +08:00
private const double RECTANGLE_ANGLE_THRESHOLD = 15.0;
2025-07-29 23:14:20 +08:00
/// <summary>
/// 矩形参考线类,用于辅助矩形绘制
/// </summary>
2025-07-29 23:14:20 +08:00
private class RectangleGuideLine
{
/// <summary>
/// 原始笔画
/// </summary>
2025-07-29 23:14:20 +08:00
public Stroke OriginalStroke { get; set; }
/// <summary>
/// 起始点
/// </summary>
2025-07-29 23:14:20 +08:00
public Point StartPoint { get; set; }
/// <summary>
/// 结束点
/// </summary>
2025-07-29 23:14:20 +08:00
public Point EndPoint { get; set; }
/// <summary>
/// 创建时间
/// </summary>
2025-07-29 23:14:20 +08:00
public DateTime CreatedTime { get; set; }
/// <summary>
/// 角度
/// </summary>
2025-12-13 20:32:43 +08:00
public double Angle { get; set; }
/// <summary>
/// 是否为水平线
/// </summary>
2025-07-29 23:14:20 +08:00
public bool IsHorizontal { get; set; }
/// <summary>
/// 是否为垂直线
/// </summary>
2025-07-29 23:14:20 +08:00
public bool IsVertical { get; set; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="stroke">原始笔画</param>
/// <param name="start">起始点</param>
/// <param name="end">结束点</param>
2025-07-29 23:14:20 +08:00
public RectangleGuideLine(Stroke stroke, Point start, Point end)
{
OriginalStroke = stroke;
StartPoint = start;
EndPoint = end;
CreatedTime = DateTime.Now;
// 计算角度
double deltaX = end.X - start.X;
double deltaY = end.Y - start.Y;
Angle = Math.Atan2(deltaY, deltaX);
// 判断是否为水平或垂直线
double angleDegrees = Math.Abs(Angle * 180.0 / Math.PI);
IsHorizontal = angleDegrees < RECTANGLE_ANGLE_THRESHOLD || angleDegrees > (180 - RECTANGLE_ANGLE_THRESHOLD);
IsVertical = Math.Abs(angleDegrees - 90) < RECTANGLE_ANGLE_THRESHOLD;
}
}
2026-04-05 12:17:02 +08:00
/// <summary>
/// 收笔后压感/墨迹平滑等尾部处理。返回「当前应登记到手写字形替换批次」的画布笔画引用:
/// 同步贝塞尔平滑若替换了笔画,则为新 <see cref="Stroke"/>;否则为 <paramref name="e"/>.Stroke。
/// 直线拉直后事件参数中的笔画可能已不在画布上,调用方需另行传入画布上的笔画(见收笔处)。
/// </summary>
private Stroke RunStrokeCollectedPostShapeRecognitionTail(InkCanvasStrokeCollectedEventArgs e, bool wasStraightened)
2026-04-04 22:16:37 +08:00
{
2026-04-05 12:17:02 +08:00
if (e?.Stroke == null)
return null;
var handwritingScheduleStroke = e.Stroke;
2026-04-04 22:16:37 +08:00
try
{
foreach (var stylusPoint in e.Stroke.StylusPoints)
if ((stylusPoint.PressureFactor > 0.501 || stylusPoint.PressureFactor < 0.5) &&
stylusPoint.PressureFactor != 0)
2026-04-05 12:17:02 +08:00
return e.Stroke;
2026-04-04 22:16:37 +08:00
try
{
if (e.Stroke.StylusPoints.Count > 3)
{
var random = new Random();
var _speed = GetPointSpeed(
e.Stroke.StylusPoints[random.Next(0, e.Stroke.StylusPoints.Count - 1)].ToPoint(),
e.Stroke.StylusPoints[random.Next(0, e.Stroke.StylusPoints.Count - 1)].ToPoint(),
e.Stroke.StylusPoints[random.Next(0, e.Stroke.StylusPoints.Count - 1)].ToPoint());
RandWindow.randSeed = (int)(_speed * 100000 * 1000);
}
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
2026-04-05 18:52:19 +08:00
// 「屏蔽压感」已在收笔主路径将点集归一成 0.5;此处若再跑 InkStyle 0/1 会重写 PressureFactor,造成假压感。
if (!Settings.Canvas.DisablePressure)
2026-04-04 22:16:37 +08:00
{
2026-04-05 18:52:19 +08:00
switch (Settings.Canvas.InkStyle)
{
case 1:
if (penType == 0)
try
2026-04-04 22:16:37 +08:00
{
2026-04-05 18:52:19 +08:00
var stylusPoints = new StylusPointCollection();
var n = e.Stroke.StylusPoints.Count - 1;
for (var i = 0; i <= n; i++)
2026-04-04 22:56:34 +08:00
{
2026-04-05 18:52:19 +08:00
var speed = GetPointSpeed(e.Stroke.StylusPoints[Math.Max(i - 1, 0)].ToPoint(),
e.Stroke.StylusPoints[i].ToPoint(),
e.Stroke.StylusPoints[Math.Min(i + 1, n)].ToPoint());
var point = new StylusPoint
{
PressureFactor = RateBasedPressureFactorFromPointSpeed(speed),
X = e.Stroke.StylusPoints[i].X,
Y = e.Stroke.StylusPoints[i].Y
};
stylusPoints.Add(point);
}
2026-04-04 22:16:37 +08:00
2026-04-05 18:52:19 +08:00
e.Stroke.StylusPoints = stylusPoints;
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
2026-04-04 22:16:37 +08:00
2026-04-05 18:52:19 +08:00
break;
case 0:
if (penType == 0)
try
2026-04-04 22:16:37 +08:00
{
2026-04-05 18:52:19 +08:00
var stylusPoints = new StylusPointCollection();
var n = e.Stroke.StylusPoints.Count - 1;
var pressure = 0.1;
var x = 10;
if (n == 1) return e.Stroke;
if (n >= x)
2026-04-04 22:16:37 +08:00
{
2026-04-05 18:52:19 +08:00
for (var i = 0; i < n - x; i++)
{
var point = new StylusPoint();
2026-04-04 22:16:37 +08:00
2026-04-05 18:52:19 +08:00
point.PressureFactor = (float)0.5;
point.X = e.Stroke.StylusPoints[i].X;
point.Y = e.Stroke.StylusPoints[i].Y;
stylusPoints.Add(point);
}
2026-04-04 22:16:37 +08:00
2026-04-05 18:52:19 +08:00
for (var i = n - x; i <= n; i++)
{
var point = new StylusPoint();
2026-04-04 22:16:37 +08:00
2026-04-05 18:52:19 +08:00
point.PressureFactor = (float)((0.5 - pressure) * (n - i) / x + pressure);
point.X = e.Stroke.StylusPoints[i].X;
point.Y = e.Stroke.StylusPoints[i].Y;
stylusPoints.Add(point);
}
2026-04-04 22:16:37 +08:00
}
2026-04-05 18:52:19 +08:00
else
2026-04-04 22:16:37 +08:00
{
2026-04-05 18:52:19 +08:00
for (var i = 0; i <= n; i++)
{
var point = new StylusPoint();
2026-04-04 22:16:37 +08:00
2026-04-05 18:52:19 +08:00
point.PressureFactor = (float)(0.4 * (n - i) / n + pressure);
point.X = e.Stroke.StylusPoints[i].X;
point.Y = e.Stroke.StylusPoints[i].Y;
stylusPoints.Add(point);
}
2026-04-04 22:16:37 +08:00
}
2026-04-05 18:52:19 +08:00
e.Stroke.StylusPoints = stylusPoints;
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
2026-04-04 22:16:37 +08:00
2026-04-05 18:52:19 +08:00
break;
case 3:
break;
}
2026-04-04 22:16:37 +08:00
}
}
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
Debug.WriteLine($"墨迹平滑检查: UseAdvancedBezierSmoothing={Settings.Canvas.UseAdvancedBezierSmoothing}, wasStraightened={wasStraightened}");
Debug.WriteLine($"异步平滑设置: UseAsyncInkSmoothing={Settings.Canvas.UseAsyncInkSmoothing}, _inkSmoothingManager={_inkSmoothingManager != null}");
if (Settings.Canvas.UseAdvancedBezierSmoothing && !wasStraightened)
{
try
{
Debug.WriteLine($"开始墨迹平滑处理: 原始点数={e.Stroke.StylusPoints.Count}, 直线拉直={wasStraightened}");
if (inkCanvas.Strokes.Contains(e.Stroke))
{
if (Settings.Canvas.UseAsyncInkSmoothing && _inkSmoothingManager != null)
{
Debug.WriteLine("使用异步墨迹平滑");
_ = ProcessStrokeAsync(e.Stroke);
}
else
{
var smoothedStroke = _inkSmoothingManager?.SmoothStroke(e.Stroke) ?? e.Stroke;
if (smoothedStroke != e.Stroke)
{
SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition;
inkCanvas.Strokes.Remove(e.Stroke);
inkCanvas.Strokes.Add(smoothedStroke);
_currentCommitType = CommitReason.UserInput;
2026-04-05 12:17:02 +08:00
handwritingScheduleStroke = smoothedStroke;
2026-04-04 22:16:37 +08:00
}
}
}
else
{
Debug.WriteLine("原始笔画不在画布中,跳过平滑处理");
}
}
catch (Exception ex)
{
Debug.WriteLine($"高级贝塞尔曲线平滑失败: {ex.Message}");
}
}
else if (Settings.Canvas.FitToCurve && !wasStraightened)
{
drawingAttributes.FitToCurve = true;
}
2026-04-05 12:17:02 +08:00
return handwritingScheduleStroke;
2026-04-04 22:16:37 +08:00
}
/// <summary>
/// 处理墨水画布的笔画收集事件
/// </summary>
/// <param name="sender">事件发送者</param>
/// <param name="e">笔画收集事件参数</param>
/// <remarks>
/// 当用户在墨水画布上完成一笔绘制后:
/// 1. 检查是否启用墨迹渐隐功能,如果启用则添加到墨迹渐隐管理器
/// 2. 根据设置处理压感:
/// - 如果禁用压感,统一压感值为0.5
/// - 如果启用触摸压感模式,根据速度模拟压感
/// 3. 如果启用了形状识别:
/// - 检查是否启用了直线自动拉直功能,如果是则尝试拉直线条
/// - 处理形状识别,包括圆形、椭圆、三角形、矩形等
/// 4. 检查是否是压感笔书写,如果是则返回
/// 5. 根据墨水风格设置模拟压感
/// 6. 应用高级贝塞尔曲线平滑(仅在未进行直线拉直时)
/// <para>
2026-03-28 17:40:14 +08:00
/// 形状识别:IACore 典型用于 32 位进程;64 位可选用 WinRTWindows 10+),详见设置「识别引擎」。
/// </para>
/// </remarks>
2025-08-03 16:46:33 +08:00
private void inkCanvas_StrokeCollected(object sender, InkCanvasStrokeCollectedEventArgs e)
{
2026-03-14 16:51:26 +08:00
var strokeDrawingAttributes = e.Stroke?.DrawingAttributes;
bool isBoardBrushStroke = strokeDrawingAttributes != null
&& !strokeDrawingAttributes.IsHighlighter
&& strokeDrawingAttributes.StylusTip == StylusTip.Rectangle
&& Math.Abs(strokeDrawingAttributes.Width - BoardBrushInkWidth) < 0.01
&& Math.Abs(strokeDrawingAttributes.Height - BoardBrushInkHeight) < 0.01;
2026-04-05 12:17:02 +08:00
// 手写识别须与画布显示分离:在压感/触摸模拟/笔锋/直线拉直等修改 e.Stroke 之前快照原始落笔点集。
var handwritingRawPointsForRecognizer =
CloneStylusPointCollectionForHandwritingInput(e.Stroke?.StylusPoints);
2025-08-23 23:13:39 +08:00
// 检查是否启用墨迹渐隐功能
2026-03-14 16:51:26 +08:00
if (Settings.Canvas.EnableInkFade && !isBoardBrushStroke)
2025-08-23 23:13:39 +08:00
{
// 获取墨迹的起点和终点
var startPoint = e.Stroke.StylusPoints.Count > 0 ? e.Stroke.StylusPoints[0].ToPoint() : new Point();
var endPoint = e.Stroke.StylusPoints.Count > 0 ? e.Stroke.StylusPoints[e.Stroke.StylusPoints.Count - 1].ToPoint() : new Point();
2025-08-31 11:43:52 +08:00
2025-09-30 19:16:56 +08:00
if (inkCanvas.EditingMode != InkCanvasEditingMode.Ink)
2025-08-23 23:13:39 +08:00
{
2025-09-30 19:16:56 +08:00
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
2025-08-23 23:13:39 +08:00
}
2025-08-31 11:43:52 +08:00
2025-08-23 23:13:39 +08:00
// 添加到墨迹渐隐管理器
if (_inkFadeManager != null)
{
_inkFadeManager.AddFadingStroke(e.Stroke, startPoint, endPoint);
}
else
{
LogHelper.WriteLogToFile("StrokeCollected: 墨迹渐隐管理器为空,无法添加墨迹", LogHelper.LogType.Error);
}
2025-08-31 11:43:52 +08:00
2025-09-30 19:16:56 +08:00
Dispatcher.BeginInvoke(new Action(() =>
{
try
{
if (inkCanvas.EditingMode != InkCanvasEditingMode.Ink)
{
inkCanvas.EditingMode = InkCanvasEditingMode.Ink;
}
if (inkCanvas.Strokes.Contains(e.Stroke))
{
inkCanvas.Strokes.Remove(e.Stroke);
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"延迟移除墨迹时出错: {ex}", LogHelper.LogType.Error);
}
}), DispatcherPriority.Background);
2025-08-23 23:13:39 +08:00
return;
}
2025-08-31 11:43:52 +08:00
2025-07-20 15:57:51 +08:00
// 标记是否进行了直线拉直
bool wasStraightened = false;
2026-04-05 12:17:02 +08:00
StylusPointCollection preBrushHandwritingPoints = null;
Stroke strokeForHandwritingBeautify = null;
2025-08-03 16:46:33 +08:00
2025-07-28 14:40:44 +08:00
if (Settings.Canvas.FitToCurve) drawingAttributes.FitToCurve = false;
2025-05-25 09:29:48 +08:00
2025-08-03 16:46:33 +08:00
try
{
2025-05-25 09:29:48 +08:00
inkCanvas.Opacity = 1;
2026-03-28 16:59:02 +08:00
var touchPressureSimulationApplied = false;
2026-04-05 12:17:02 +08:00
preBrushHandwritingPoints = handwritingRawPointsForRecognizer;
2025-08-03 16:46:33 +08:00
if (Settings.Canvas.DisablePressure)
{
2025-06-18 13:37:52 +08:00
var uniformPoints = new StylusPointCollection();
2025-08-03 16:46:33 +08:00
foreach (StylusPoint point in e.Stroke.StylusPoints)
{
2025-06-18 13:37:52 +08:00
StylusPoint newPoint = new StylusPoint(point.X, point.Y, 0.5f); // 统一压感值为0.5
uniformPoints.Add(newPoint);
}
e.Stroke.StylusPoints = uniformPoints;
}
2025-08-03 16:46:33 +08:00
else if (Settings.Canvas.EnablePressureTouchMode)
{
2025-06-18 13:37:52 +08:00
bool isTouchInput = true;
2025-08-03 16:46:33 +08:00
foreach (StylusPoint point in e.Stroke.StylusPoints)
{
if ((point.PressureFactor > 0.501 || point.PressureFactor < 0.5) && point.PressureFactor != 0)
{
2025-06-18 13:37:52 +08:00
isTouchInput = false;
break;
}
}
2025-08-03 16:46:33 +08:00
if (isTouchInput)
{
switch (Settings.Canvas.InkStyle)
{
2025-06-18 13:37:52 +08:00
case 1:
if (penType == 0)
2025-08-03 16:46:33 +08:00
try
{
2025-06-18 13:37:52 +08:00
var stylusPoints = new StylusPointCollection();
var n = e.Stroke.StylusPoints.Count - 1;
2025-08-03 16:46:33 +08:00
for (var i = 0; i <= n; i++)
{
2025-06-18 13:37:52 +08:00
var speed = GetPointSpeed(e.Stroke.StylusPoints[Math.Max(i - 1, 0)].ToPoint(),
e.Stroke.StylusPoints[i].ToPoint(),
e.Stroke.StylusPoints[Math.Min(i + 1, n)].ToPoint());
2026-04-04 22:56:34 +08:00
var point = new StylusPoint
{
PressureFactor = RateBasedPressureFactorFromPointSpeed(speed),
X = e.Stroke.StylusPoints[i].X,
Y = e.Stroke.StylusPoints[i].Y
};
2025-06-18 13:37:52 +08:00
stylusPoints.Add(point);
}
2026-03-28 16:59:02 +08:00
touchPressureSimulationApplied = true;
2025-06-18 13:37:52 +08:00
e.Stroke.StylusPoints = stylusPoints;
}
2026-02-21 16:51:34 +08:00
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
2025-06-18 13:37:52 +08:00
break;
case 0:
if (penType == 0)
2025-08-03 16:46:33 +08:00
try
{
2025-06-18 13:37:52 +08:00
var stylusPoints = new StylusPointCollection();
var n = e.Stroke.StylusPoints.Count - 1;
var pressure = 0.1;
var x = 10;
if (n == 1) return;
2025-08-03 16:46:33 +08:00
if (n >= x)
{
for (var i = 0; i < n - x; i++)
{
2025-06-18 13:37:52 +08:00
var point = new StylusPoint();
point.PressureFactor = (float)0.5;
point.X = e.Stroke.StylusPoints[i].X;
point.Y = e.Stroke.StylusPoints[i].Y;
stylusPoints.Add(point);
}
2025-08-03 16:46:33 +08:00
for (var i = n - x; i <= n; i++)
{
2025-06-18 13:37:52 +08:00
var point = new StylusPoint();
point.PressureFactor = (float)((0.5 - pressure) * (n - i) / x + pressure);
point.X = e.Stroke.StylusPoints[i].X;
point.Y = e.Stroke.StylusPoints[i].Y;
stylusPoints.Add(point);
}
}
2025-08-03 16:46:33 +08:00
else
{
for (var i = 0; i <= n; i++)
{
2025-06-18 13:37:52 +08:00
var point = new StylusPoint();
point.PressureFactor = (float)(0.4 * (n - i) / n + pressure);
point.X = e.Stroke.StylusPoints[i].X;
point.Y = e.Stroke.StylusPoints[i].Y;
stylusPoints.Add(point);
}
}
2026-03-28 16:59:02 +08:00
touchPressureSimulationApplied = true;
2025-06-18 13:37:52 +08:00
e.Stroke.StylusPoints = stylusPoints;
}
2026-02-21 16:51:34 +08:00
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
2025-06-18 13:37:52 +08:00
break;
}
}
}
2025-08-03 16:46:33 +08:00
2026-04-04 22:16:37 +08:00
// 实时笔锋:勿依赖 DrawingAttributes.IgnorePressure。无压感触摸/鼠标等设备上,运行时仍可能为 true,
// 会导致不进入逻辑或进入后渲染仍忽略 PressureFactor;具体在 ApplyVelocityBrushTipFromSpeed 内关闭。
2026-04-05 18:52:19 +08:00
// 「屏蔽压感」时必须跳过:否则会重写 PressureFactor 并强制 IgnorePressure=false,与归一压感冲突。
// VelocityBrushTipMix <= 0 时 ApplyVelocityBrushTipFromSpeed 为空操作,无需调用。
2026-03-28 17:04:50 +08:00
if (Settings.Canvas.InkStyle == 3
2026-04-05 18:52:19 +08:00
&& Settings.Canvas.VelocityBrushTipMix > 0
&& !Settings.Canvas.DisablePressure
2026-03-28 16:59:02 +08:00
&& !touchPressureSimulationApplied
&& penType != 1
&& e.Stroke?.DrawingAttributes != null
&& !e.Stroke.DrawingAttributes.IsHighlighter
2026-04-25 17:27:28 +08:00
&& !e.Stroke.ContainsPropertyData(RealtimeVelocityBrushTipAppliedGuid)
2026-03-28 16:59:02 +08:00
&& e.Stroke.StylusPoints.Count >= 3)
{
ApplyVelocityBrushTipFromSpeed(e.Stroke);
}
2025-08-03 16:46:33 +08:00
// Apply line straightening and endpoint snapping if ink-to-shape is enabled
2026-04-05 12:17:02 +08:00
Stroke straightStrokeForHandwritingKey = null;
2025-08-03 16:46:33 +08:00
if (Settings.InkToShape.IsInkToShapeEnabled)
{
2025-06-19 14:30:24 +08:00
// 检查是否启用了直线自动拉直功能
2025-08-03 16:46:33 +08:00
if (Settings.Canvas.AutoStraightenLine && IsPotentialStraightLine(e.Stroke))
{
2025-11-29 22:20:13 +08:00
Point endpoint1, endpoint2;
bool shouldStraighten = TryGetStraightLineEndpoints(e.Stroke, out endpoint1, out endpoint2);
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
if (shouldStraighten)
2025-08-03 16:46:33 +08:00
{
2025-11-29 22:20:13 +08:00
Point startPoint = endpoint1;
Point endPoint = endpoint2;
// 只有当确定要拉直线条时,才检查端点吸附
if (Settings.Canvas.LineEndpointSnapping)
2025-08-03 16:46:33 +08:00
{
2025-11-29 22:20:13 +08:00
// 只有在启用了形状识别(矩形或三角形)时才执行端点吸附
if (Settings.InkToShape.IsInkToShapeRectangle || Settings.InkToShape.IsInkToShapeTriangle)
2025-08-03 16:46:33 +08:00
{
2025-11-29 22:20:13 +08:00
Point[] snappedPoints = GetSnappedEndpoints(startPoint, endPoint);
if (snappedPoints != null)
{
startPoint = snappedPoints[0];
endPoint = snappedPoints[1];
}
2025-06-19 14:30:24 +08:00
}
2025-06-17 22:37:37 +08:00
}
2025-06-18 13:24:50 +08:00
2025-11-29 22:20:13 +08:00
// 创建直线
2025-06-18 13:24:50 +08:00
StylusPointCollection straightLinePoints = CreateStraightLine(startPoint, endPoint);
2025-08-03 16:46:33 +08:00
Stroke straightStroke = new Stroke(straightLinePoints)
{
2025-06-18 13:24:50 +08:00
DrawingAttributes = inkCanvas.DefaultDrawingAttributes.Clone()
};
straightStroke.AddPropertyData(Helpers.ModernInkAnalyzer.ShapeStrokePropertyGuid, true);
2025-08-03 16:46:33 +08:00
2025-06-18 13:24:50 +08:00
// Replace the original stroke with the straightened one
2026-04-30 17:51:43 +08:00
ReplaceStrokesSafely(null, straightStroke, e.Stroke);
2025-08-03 16:46:33 +08:00
2026-04-05 12:17:02 +08:00
straightStrokeForHandwritingKey = straightStroke;
2025-06-18 13:24:50 +08:00
// We can't modify e.Stroke directly, but we need to update newStrokes
// to ensure proper shape recognition for the straightened line
2025-08-03 16:46:33 +08:00
if (newStrokes.Contains(e.Stroke))
{
2025-06-18 13:24:50 +08:00
newStrokes.Remove(e.Stroke);
newStrokes.Add(straightStroke);
}
2025-08-03 16:46:33 +08:00
2025-07-20 15:57:51 +08:00
wasStraightened = true; // 标记已进行直线拉直
2025-06-17 22:37:37 +08:00
}
}
}
2025-06-18 13:24:50 +08:00
2026-04-05 12:17:02 +08:00
strokeForHandwritingBeautify = e.Stroke;
if (wasStraightened && straightStrokeForHandwritingKey != null)
strokeForHandwritingBeautify = straightStrokeForHandwritingKey;
else if (wasStraightened && inkCanvas.Strokes.Count > 0)
2026-03-29 12:24:13 +08:00
strokeForHandwritingBeautify = inkCanvas.Strokes[inkCanvas.Strokes.Count - 1];
2026-04-04 22:16:37 +08:00
2026-03-28 17:40:14 +08:00
if (ShapeRecognitionRouter.ShouldRunShapeRecognition(
Settings.InkToShape.IsInkToShapeEnabled,
ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine)))
2025-08-03 16:46:33 +08:00
{
2026-03-28 18:30:40 +08:00
async Task InkToShapeProcessCoreAsync()
2025-08-03 16:46:33 +08:00
{
2026-03-28 18:40:18 +08:00
await InkToShapeSerial.WaitAsync().ConfigureAwait(true);
2025-08-03 16:46:33 +08:00
try
{
2025-05-25 09:29:48 +08:00
newStrokes.Add(e.Stroke);
if (newStrokes.Count > 4) newStrokes.RemoveAt(0);
for (var i = 0; i < newStrokes.Count; i++)
if (!inkCanvas.Strokes.Contains(newStrokes[i]))
newStrokes.RemoveAt(i--);
for (var i = 0; i < circles.Count; i++)
if (!inkCanvas.Strokes.Contains(circles[i].Stroke))
circles.RemoveAt(i);
2025-07-29 23:14:20 +08:00
// 处理矩形参考线系统
ProcessRectangleGuideLines(e.Stroke);
2026-03-28 17:40:14 +08:00
var shapeMode = ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine);
InkShapeRecognitionResult result = InkShapeRecognitionResult.Empty;
2026-04-30 14:29:06 +08:00
if (ShapeRecognitionRouter.ResolveUseWinRt(shapeMode) && Helpers.WinRtInkShapeRecognizer.IsApiAvailable)
{
result = await ModernInkAnalyzer.AnalyzeAsync(newStrokes);
}
else
2025-08-03 16:46:33 +08:00
{
var strokeReco = new StrokeCollection();
result = await InkRecognizeHelper.RecognizeShapeUnifiedAsync(newStrokes, shapeMode);
for (var i = newStrokes.Count - 1; i >= 0; i--)
2025-08-03 16:46:33 +08:00
{
strokeReco.Add(newStrokes[i]);
var newResult = await InkRecognizeHelper.RecognizeShapeUnifiedAsync(strokeReco, shapeMode);
if (newResult.IsSuccess &&
(newResult.ShapeName == "Circle" || newResult.ShapeName == "Ellipse"))
{
result = newResult;
break;
}
2025-05-25 09:29:48 +08:00
}
}
2026-03-28 17:40:14 +08:00
if (!result.IsSuccess)
return;
if (result.ShapeName == "Circle" &&
2025-08-03 16:46:33 +08:00
Settings.InkToShape.IsInkToShapeRounded)
{
2026-03-28 17:40:14 +08:00
if (result.ShapeWidth > 75)
2025-08-03 16:46:33 +08:00
{
2025-05-25 09:29:48 +08:00
foreach (var circle in circles)
//判断是否画同心圆
2026-03-28 17:40:14 +08:00
if (Math.Abs(result.Centroid.X - circle.Centroid.X) / result.ShapeWidth < 0.12 &&
Math.Abs(result.Centroid.Y - circle.Centroid.Y) / result.ShapeWidth < 0.12)
2025-08-03 16:46:33 +08:00
{
2025-05-25 09:29:48 +08:00
result.Centroid = circle.Centroid;
break;
}
2025-08-03 16:46:33 +08:00
else
{
2025-05-25 09:29:48 +08:00
var d = (result.Centroid.X - circle.Centroid.X) *
(result.Centroid.X - circle.Centroid.X) +
(result.Centroid.Y - circle.Centroid.Y) *
(result.Centroid.Y - circle.Centroid.Y);
d = Math.Sqrt(d);
//判断是否画外切圆
2026-03-28 17:40:14 +08:00
var x = result.ShapeWidth / 2.0 + circle.R - d;
if (Math.Abs(x) / result.ShapeWidth < 0.1)
2025-08-03 16:46:33 +08:00
{
2025-05-25 09:29:48 +08:00
var sinTheta = (result.Centroid.Y - circle.Centroid.Y) / d;
var cosTheta = (result.Centroid.X - circle.Centroid.X) / d;
var newX = result.Centroid.X + x * cosTheta;
var newY = result.Centroid.Y + x * sinTheta;
result.Centroid = new Point(newX, newY);
}
//判断是否画外切圆
2026-03-28 17:40:14 +08:00
x = Math.Abs(circle.R - result.ShapeWidth / 2.0) - d;
if (Math.Abs(x) / result.ShapeWidth < 0.1)
2025-08-03 16:46:33 +08:00
{
2025-05-25 09:29:48 +08:00
var sinTheta = (result.Centroid.Y - circle.Centroid.Y) / d;
var cosTheta = (result.Centroid.X - circle.Centroid.X) / d;
var newX = result.Centroid.X + x * cosTheta;
var newY = result.Centroid.Y + x * sinTheta;
result.Centroid = new Point(newX, newY);
}
}
2026-03-28 17:40:14 +08:00
var iniP = new Point(result.Centroid.X - result.ShapeWidth / 2,
result.Centroid.Y - result.ShapeHeight / 2);
var endP = new Point(result.Centroid.X + result.ShapeWidth / 2,
result.Centroid.Y + result.ShapeHeight / 2);
2025-05-25 09:29:48 +08:00
var pointList = GenerateEllipseGeometry(iniP, endP);
var point = new StylusPointCollection(pointList);
2025-08-03 16:46:33 +08:00
var stroke = new Stroke(point)
{
2025-05-25 09:29:48 +08:00
DrawingAttributes = inkCanvas.DefaultDrawingAttributes.Clone()
};
stroke.AddPropertyData(Helpers.ModernInkAnalyzer.ShapeStrokePropertyGuid, true);
2026-03-28 17:40:14 +08:00
circles.Add(new Circle(result.Centroid, result.ShapeWidth / 2.0, stroke));
2026-04-30 17:51:43 +08:00
ReplaceStrokesSafely(result.StrokesToRemove, stroke, e.Stroke);
2025-05-25 09:29:48 +08:00
newStrokes = new StrokeCollection();
}
}
2026-03-28 17:40:14 +08:00
else if (result.ShapeName.Contains("Ellipse") &&
2025-08-03 16:46:33 +08:00
Settings.InkToShape.IsInkToShapeRounded)
{
2026-03-28 17:40:14 +08:00
var p = result.HotPoints;
2025-05-25 09:29:48 +08:00
var a = GetDistance(p[0], p[2]) / 2; //长半轴
var b = GetDistance(p[1], p[3]) / 2; //短半轴
2025-08-03 16:46:33 +08:00
if (a < b)
{
2025-05-25 09:29:48 +08:00
var t = a;
a = b;
b = t;
}
result.Centroid = new Point((p[0].X + p[2].X) / 2, (p[0].Y + p[2].Y) / 2);
var needRotation = true;
2026-03-28 17:40:14 +08:00
if (result.ShapeWidth > 75 || (result.ShapeHeight > 75 && p.Count == 4))
2025-08-03 16:46:33 +08:00
{
2026-03-28 17:40:14 +08:00
var iniP = new Point(result.Centroid.X - result.ShapeWidth / 2,
result.Centroid.Y - result.ShapeHeight / 2);
var endP = new Point(result.Centroid.X + result.ShapeWidth / 2,
result.Centroid.Y + result.ShapeHeight / 2);
2025-05-25 09:29:48 +08:00
2026-04-18 17:10:27 +08:00
// WinRT 返回的热点顺序/方向不稳定时,用点集反推 IACore 风格椭圆参数(中心/长短轴/方向/四个端点)
var hasEllipseParams = TryEstimateEllipseParamsFromStrokes(
result.StrokesToRemove,
out var ellipseCentroid,
out var ellipseA,
out var ellipseB,
out var ellipseThetaRad,
out var ellipseMajor0,
out var ellipseMajor1,
out var ellipseMinor0,
out var ellipseMinor1);
2025-05-25 09:29:48 +08:00
foreach (var circle in circles)
//判断是否画同心椭圆
if (Math.Abs(result.Centroid.X - circle.Centroid.X) / a < 0.2 &&
2025-08-03 16:46:33 +08:00
Math.Abs(result.Centroid.Y - circle.Centroid.Y) / a < 0.2)
{
2025-05-25 09:29:48 +08:00
result.Centroid = circle.Centroid;
2026-03-28 17:40:14 +08:00
iniP = new Point(result.Centroid.X - result.ShapeWidth / 2,
result.Centroid.Y - result.ShapeHeight / 2);
endP = new Point(result.Centroid.X + result.ShapeWidth / 2,
result.Centroid.Y + result.ShapeHeight / 2);
2025-05-25 09:29:48 +08:00
//再判断是否与圆相切
2025-08-03 16:46:33 +08:00
if (Math.Abs(a - circle.R) / a < 0.2)
{
2026-03-28 17:40:14 +08:00
if (result.ShapeWidth >= result.ShapeHeight)
2025-08-03 16:46:33 +08:00
{
2025-05-25 09:29:48 +08:00
iniP.X = result.Centroid.X - circle.R;
endP.X = result.Centroid.X + circle.R;
iniP.Y = result.Centroid.Y - b;
endP.Y = result.Centroid.Y + b;
}
2025-08-03 16:46:33 +08:00
else
{
2025-05-25 09:29:48 +08:00
iniP.Y = result.Centroid.Y - circle.R;
endP.Y = result.Centroid.Y + circle.R;
iniP.X = result.Centroid.X - a;
endP.X = result.Centroid.X + a;
}
}
break;
}
2025-08-03 16:46:33 +08:00
else if (Math.Abs(result.Centroid.X - circle.Centroid.X) / a < 0.2)
{
2025-05-25 09:29:48 +08:00
var sinTheta = Math.Abs(circle.Centroid.Y - result.Centroid.Y) /
circle.R;
var cosTheta = Math.Sqrt(1 - sinTheta * sinTheta);
var newA = circle.R * cosTheta;
if (circle.R * sinTheta / circle.R < 0.9 && a / b > 2 &&
2025-08-03 16:46:33 +08:00
Math.Abs(newA - a) / newA < 0.3)
{
2025-05-25 09:29:48 +08:00
iniP.X = circle.Centroid.X - newA;
endP.X = circle.Centroid.X + newA;
iniP.Y = result.Centroid.Y - newA / 5;
endP.Y = result.Centroid.Y + newA / 5;
var topB = endP.Y - iniP.Y;
newStrokes = new StrokeCollection();
2025-07-28 14:40:44 +08:00
var _pointList = GenerateEllipseGeometry(iniP, endP, false);
2025-05-25 09:29:48 +08:00
var _point = new StylusPointCollection(_pointList);
2025-08-03 16:46:33 +08:00
var _stroke = new Stroke(_point)
{
2025-05-25 09:29:48 +08:00
DrawingAttributes = inkCanvas.DefaultDrawingAttributes.Clone()
};
_stroke.AddPropertyData(Helpers.ModernInkAnalyzer.ShapeStrokePropertyGuid, true);
2025-05-25 09:29:48 +08:00
var _dashedLineStroke =
GenerateDashedLineEllipseStrokeCollection(iniP, endP, true, false);
2025-07-28 14:40:44 +08:00
var strokes = new StrokeCollection {
2025-05-25 09:29:48 +08:00
_stroke,
_dashedLineStroke
};
2026-04-30 17:51:43 +08:00
ReplaceStrokesSafely(result.StrokesToRemove, strokes, e.Stroke);
2025-05-25 09:29:48 +08:00
return;
}
}
2025-08-03 16:46:33 +08:00
else if (Math.Abs(result.Centroid.Y - circle.Centroid.Y) / a < 0.2)
{
2025-05-25 09:29:48 +08:00
var cosTheta = Math.Abs(circle.Centroid.X - result.Centroid.X) /
circle.R;
var sinTheta = Math.Sqrt(1 - cosTheta * cosTheta);
var newA = circle.R * sinTheta;
if (circle.R * sinTheta / circle.R < 0.9 && a / b > 2 &&
2025-08-03 16:46:33 +08:00
Math.Abs(newA - a) / newA < 0.3)
{
2025-05-25 09:29:48 +08:00
iniP.X = result.Centroid.X - newA / 5;
endP.X = result.Centroid.X + newA / 5;
iniP.Y = circle.Centroid.Y - newA;
endP.Y = circle.Centroid.Y + newA;
needRotation = false;
}
}
2026-04-18 17:10:27 +08:00
// 用反推参数替换中心与长短轴(比 WinRT 的包围盒更接近 IACore,且不会竖横翻转)
if (hasEllipseParams)
{
result.Centroid = ellipseCentroid;
a = ellipseA;
b = ellipseB;
iniP = new Point(result.Centroid.X - a, result.Centroid.Y - b);
endP = new Point(result.Centroid.X + a, result.Centroid.Y + b);
// 用端点重写热点,保证后续回退分支也一致
p = new PointCollection { ellipseMajor0, ellipseMinor0, ellipseMajor1, ellipseMinor1 };
}
2025-05-25 09:29:48 +08:00
var pointList = GenerateEllipseGeometry(iniP, endP);
var point = new StylusPointCollection(pointList);
2025-08-03 16:46:33 +08:00
var stroke = new Stroke(point)
{
2025-05-25 09:29:48 +08:00
DrawingAttributes = inkCanvas.DefaultDrawingAttributes.Clone()
};
stroke.AddPropertyData(Helpers.ModernInkAnalyzer.ShapeStrokePropertyGuid, true);
2025-05-25 09:29:48 +08:00
2025-08-03 16:46:33 +08:00
if (needRotation)
{
2025-05-25 09:29:48 +08:00
var m = new Matrix();
2026-04-18 17:10:27 +08:00
// 优先使用反推参数角度;否则用端点向量角度(使用 Atan2 避免斜率无穷)
var theta = hasEllipseParams ? ellipseThetaRad : Math.Atan2(p[2].Y - p[0].Y, p[2].X - p[0].X);
2025-05-25 09:29:48 +08:00
m.RotateAt(theta * 180.0 / Math.PI, result.Centroid.X, result.Centroid.Y);
stroke.Transform(m, false);
}
2026-04-30 17:51:43 +08:00
ReplaceStrokesSafely(result.StrokesToRemove, stroke, e.Stroke);
2025-05-25 09:29:48 +08:00
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
newStrokes = new StrokeCollection();
}
}
2026-03-28 17:40:14 +08:00
else if (result.ShapeName.Contains("Triangle") &&
2025-08-03 16:46:33 +08:00
Settings.InkToShape.IsInkToShapeTriangle)
{
2026-03-28 17:40:14 +08:00
var p = result.HotPoints;
2025-05-25 09:29:48 +08:00
if ((Math.Max(Math.Max(p[0].X, p[1].X), p[2].X) -
Math.Min(Math.Min(p[0].X, p[1].X), p[2].X) >= 100 ||
Math.Max(Math.Max(p[0].Y, p[1].Y), p[2].Y) -
Math.Min(Math.Min(p[0].Y, p[1].Y), p[2].Y) >= 100) &&
2026-03-28 17:40:14 +08:00
result.HotPoints.Count == 3)
2025-08-03 16:46:33 +08:00
{
2025-05-25 09:29:48 +08:00
//纠正垂直与水平关系
var newPoints = FixPointsDirection(p[0], p[1]);
p[0] = newPoints[0];
p[1] = newPoints[1];
newPoints = FixPointsDirection(p[0], p[2]);
p[0] = newPoints[0];
p[2] = newPoints[1];
newPoints = FixPointsDirection(p[1], p[2]);
p[1] = newPoints[0];
p[2] = newPoints[1];
var pointList = p.ToList();
//pointList.Add(p[0]);
var point = new StylusPointCollection(pointList);
2025-08-03 16:46:33 +08:00
var stroke = new Stroke(GenerateFakePressureTriangle(point))
{
2025-05-25 09:29:48 +08:00
DrawingAttributes = inkCanvas.DefaultDrawingAttributes.Clone()
};
stroke.AddPropertyData(Helpers.ModernInkAnalyzer.ShapeStrokePropertyGuid, true);
2026-04-30 17:51:43 +08:00
ReplaceStrokesSafely(result.StrokesToRemove, stroke, e.Stroke);
2025-05-25 09:29:48 +08:00
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
newStrokes = new StrokeCollection();
}
}
2026-03-28 17:40:14 +08:00
else if ((result.ShapeName.Contains("Rectangle") ||
result.ShapeName.Contains("Diamond") ||
result.ShapeName.Contains("Parallelogram") ||
result.ShapeName.Contains("Square") ||
2026-03-28 17:52:30 +08:00
result.ShapeName.Contains("Trapezoid") ||
result.ShapeName.Contains("Quadrilateral")) &&
2025-08-03 16:46:33 +08:00
Settings.InkToShape.IsInkToShapeRectangle)
{
2026-03-28 17:40:14 +08:00
var p = result.HotPoints;
2025-05-25 09:29:48 +08:00
if ((Math.Max(Math.Max(Math.Max(p[0].X, p[1].X), p[2].X), p[3].X) -
Math.Min(Math.Min(Math.Min(p[0].X, p[1].X), p[2].X), p[3].X) >= 100 ||
Math.Max(Math.Max(Math.Max(p[0].Y, p[1].Y), p[2].Y), p[3].Y) -
Math.Min(Math.Min(Math.Min(p[0].Y, p[1].Y), p[2].Y), p[3].Y) >= 100) &&
2026-03-28 17:40:14 +08:00
result.HotPoints.Count == 4)
2025-08-03 16:46:33 +08:00
{
2025-05-25 09:29:48 +08:00
//纠正垂直与水平关系
var newPoints = FixPointsDirection(p[0], p[1]);
p[0] = newPoints[0];
p[1] = newPoints[1];
newPoints = FixPointsDirection(p[1], p[2]);
p[1] = newPoints[0];
p[2] = newPoints[1];
newPoints = FixPointsDirection(p[2], p[3]);
p[2] = newPoints[0];
p[3] = newPoints[1];
newPoints = FixPointsDirection(p[3], p[0]);
p[3] = newPoints[0];
p[0] = newPoints[1];
var pointList = p.ToList();
pointList.Add(p[0]);
var point = new StylusPointCollection(pointList);
2025-08-03 16:46:33 +08:00
var stroke = new Stroke(GenerateFakePressureRectangle(point))
{
2025-05-25 09:29:48 +08:00
DrawingAttributes = inkCanvas.DefaultDrawingAttributes.Clone()
};
stroke.AddPropertyData(Helpers.ModernInkAnalyzer.ShapeStrokePropertyGuid, true);
2026-04-30 17:51:43 +08:00
ReplaceStrokesSafely(result.StrokesToRemove, stroke, e.Stroke);
2025-05-25 09:29:48 +08:00
GridInkCanvasSelectionCover.Visibility = Visibility.Collapsed;
newStrokes = new StrokeCollection();
}
}
}
2026-02-21 16:51:34 +08:00
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
2026-03-28 18:40:18 +08:00
finally
{
InkToShapeSerial.Release();
}
2026-03-28 18:30:40 +08:00
}
2026-04-04 22:16:37 +08:00
bool InkToShapeProcess()
2026-03-28 18:30:40 +08:00
{
2026-04-04 23:06:16 +08:00
// WinRT 与非 WinRT 均延后到 Dispatcher 上异步执行:在 StrokeCollected 内对
// InkToShapeProcessCoreAsync 同步 GetResult() 会长时间阻塞 UI 线程(抬笔卡顿);
// WinRT 路径若同步等待还可能与贴回 UI 的延续死锁(见类注释 InkToShapeSerial)。
var strokeHw = strokeForHandwritingBeautify;
var preBrushHwPts = preBrushHandwritingPoints;
var wsTail = wasStraightened;
// ApplicationIdle:在更高优先级(布局/输入/本帧渲染)之后执行,减轻抬笔瞬间主线程“顶死”感。
Dispatcher.BeginInvoke(new Action(async () =>
2026-03-28 18:30:40 +08:00
{
2026-04-04 23:06:16 +08:00
try
2026-03-28 18:30:40 +08:00
{
2026-04-04 23:06:16 +08:00
await InkToShapeProcessCoreAsync();
2026-04-05 12:17:02 +08:00
var strokeAfterTail = RunStrokeCollectedPostShapeRecognitionTail(e, wsTail);
2026-04-04 23:06:16 +08:00
if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify)
2026-04-05 12:17:02 +08:00
{
var canvasStrokeForHw = wsTail ? strokeHw : strokeAfterTail;
ScheduleHandwritingGlyphReplaceAfterStrokeCollected(
canvasStrokeForHw,
isBoardBrushStroke,
preBrushHwPts);
}
2026-04-04 23:06:16 +08:00
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
}), DispatcherPriority.ApplicationIdle);
return true;
2025-05-25 09:29:48 +08:00
}
2026-04-04 22:16:37 +08:00
if (InkToShapeProcess())
2025-06-18 13:24:50 +08:00
return;
2025-05-25 09:29:48 +08:00
}
}
2026-02-21 16:51:34 +08:00
catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); }
2025-05-25 09:29:48 +08:00
2026-04-05 12:17:02 +08:00
var strokeAfterTailSync = RunStrokeCollectedPostShapeRecognitionTail(e, wasStraightened);
if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify
&& !ShapeRecognitionRouter.ShouldRunShapeRecognition(
Settings.InkToShape.IsInkToShapeEnabled,
ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine)))
{
var canvasStrokeForHw = wasStraightened ? strokeForHandwritingBeautify : strokeAfterTailSync;
ScheduleHandwritingGlyphReplaceAfterStrokeCollected(
canvasStrokeForHw,
isBoardBrushStroke,
preBrushHandwritingPoints);
}
2025-05-25 09:29:48 +08:00
}
2025-07-26 19:03:07 +08:00
/// <summary>
/// 异步处理笔画平滑
/// </summary>
/// <param name="originalStroke">原始笔画</param>
/// <returns>返回一个表示异步操作的Task</returns>
/// <remarks>
/// 异步处理笔画平滑的流程:
/// 1. 调用墨迹平滑管理器的SmoothStrokeAsync方法
/// 2. 在平滑完成后,在UI线程上执行笔画替换
/// 3. 如果原始笔画仍然存在于画布中且平滑后的笔画不同,则替换原始笔画
/// 4. 捕获并记录可能的异常
/// </remarks>
2025-07-26 19:03:07 +08:00
private async Task ProcessStrokeAsync(Stroke originalStroke)
{
try
{
2025-09-20 13:44:01 +08:00
Debug.WriteLine($"异步平滑开始: 原始点数={originalStroke.StylusPoints.Count}");
2025-07-26 19:03:07 +08:00
await _inkSmoothingManager.SmoothStrokeAsync(originalStroke, (original, smoothed) =>
{
2025-09-20 13:44:01 +08:00
Debug.WriteLine($"异步平滑完成: 原始点数={original.StylusPoints.Count}, 平滑后点数={smoothed.StylusPoints.Count}");
Debug.WriteLine($"墨迹比较: smoothed != original = {smoothed != original}");
Debug.WriteLine($"画布包含原始墨迹: {inkCanvas.Strokes.Contains(original)}");
2025-10-03 17:08:46 +08:00
2025-07-26 19:03:07 +08:00
// 在UI线程上执行笔画替换
if (inkCanvas.Strokes.Contains(original) && smoothed != original)
{
2025-09-20 13:44:01 +08:00
Debug.WriteLine("异步替换原始笔画为平滑后的笔画");
2025-07-26 19:03:07 +08:00
SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition;
inkCanvas.Strokes.Remove(original);
inkCanvas.Strokes.Add(smoothed);
_currentCommitType = CommitReason.UserInput;
2026-04-05 12:17:02 +08:00
// 收笔尾部仍以 original 登记手写批次;异步平滑后画布对象变为 smoothed,须迁移引用,否则防抖识别时字典 miss 会退回画布几何(非实时笔锋常见)。
MigrateHandwritingBeautifyCanvasStrokeReference(original, smoothed);
2025-07-26 19:03:07 +08:00
}
2025-09-20 13:44:01 +08:00
else
{
Debug.WriteLine($"异步平滑后的笔画与原始笔画相同,未进行替换 (contains={inkCanvas.Strokes.Contains(original)}, different={smoothed != original})");
}
2025-07-26 19:03:07 +08:00
});
}
catch (Exception ex)
{
2025-07-28 14:40:44 +08:00
Debug.WriteLine($"异步墨迹平滑失败: {ex.Message}");
2025-07-26 19:03:07 +08:00
}
}
/// <summary>
/// 检查一笔墨迹是否可能是直线
/// </summary>
/// <param name="stroke">要检查的笔画</param>
/// <returns>如果可能是直线则返回true,否则返回false</returns>
/// <remarks>
/// 检查一笔墨迹是否可能是直线的流程:
/// 1. 确保有足够的点来进行线条分析(至少5个点)
/// 2. 计算线条长度,确保线条足够长(使用分辨率自适应阈值)
/// 3. 检查墨迹复杂度,避免将复杂图形拉直
/// 4. 检查是否为明显的曲线
/// 5. 根据用户设置的灵敏度值计算阈值
/// 6. 快速检查:计算几个关键点与直线的距离
/// 7. 根据偏差阈值判断是否可能是直线
/// </remarks>
2025-08-03 16:46:33 +08:00
private bool IsPotentialStraightLine(Stroke stroke)
{
2025-06-18 15:10:33 +08:00
// 确保有足够的点来进行线条分析
2025-07-29 01:40:35 +08:00
if (stroke.StylusPoints.Count < 5)
2025-06-18 13:24:50 +08:00
return false;
2025-07-29 01:40:35 +08:00
2025-06-18 13:24:50 +08:00
Point start = stroke.StylusPoints.First().ToPoint();
Point end = stroke.StylusPoints.Last().ToPoint();
double lineLength = GetDistance(start, end);
2025-07-24 21:02:00 +08:00
// 分辨率自适应阈值
double adaptiveThreshold = Settings.Canvas.AutoStraightenLineThreshold * GetResolutionScale();
// 线条必须足够长才考虑拉直,使用自适应阈值
if (lineLength < adaptiveThreshold)
2025-06-18 15:10:33 +08:00
return false;
2025-07-29 01:40:35 +08:00
// 新增:检查墨迹复杂度,避免将复杂图形拉直
if (IsComplexShape(stroke))
return false;
// 新增:检查是否为明显的曲线
if (IsObviousCurve(stroke))
return false;
2025-08-03 16:46:33 +08:00
// 获取用户设置的灵敏度值,确保使用正确的设置
2025-06-18 15:10:33 +08:00
double sensitivity = Settings.InkToShape.LineStraightenSensitivity;
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 输出当前灵敏度值
2025-07-28 14:40:44 +08:00
Debug.WriteLine($"IsPotentialStraightLine - sensitivity: {sensitivity}, length: {lineLength}");
2025-08-03 16:46:33 +08:00
2025-09-20 19:03:00 +08:00
// 将灵敏度转换为阈值:灵敏度0.05-2.0映射到阈值0.01-0.4
double quickThreshold = Math.Max(0.01, sensitivity * 0.2); // 确保最小阈值为0.01
2025-08-03 16:46:33 +08:00
2025-07-28 14:40:44 +08:00
Debug.WriteLine($"使用快速检查阈值: {quickThreshold}");
2025-08-03 16:46:33 +08:00
2025-06-18 15:10:33 +08:00
// 快速检查:计算几个关键点与直线的距离
2025-08-03 16:46:33 +08:00
if (stroke.StylusPoints.Count >= 10)
{
2025-12-06 22:06:28 +08:00
List<Point> checkPoints;
2025-12-20 13:56:46 +08:00
2025-12-06 22:06:28 +08:00
// 使用采样点进行更准确的判断
if (Settings.Canvas.HighPrecisionLineStraighten)
{
var allPoints = stroke.StylusPoints.Select(p => p.ToPoint()).ToList();
checkPoints = SamplePointsByDistance(allPoints, 10.0);
Debug.WriteLine($"高精度模式快速检查:原始点数={allPoints.Count}, 采样点数={checkPoints.Count}");
}
else
{
// 取中点和1/4、3/4位置的点
int quarterIdx = stroke.StylusPoints.Count / 4;
int midIdx = stroke.StylusPoints.Count / 2;
int threeQuarterIdx = quarterIdx * 3;
2025-08-03 16:46:33 +08:00
2025-12-06 22:06:28 +08:00
checkPoints = new List<Point>
{
stroke.StylusPoints[quarterIdx].ToPoint(),
stroke.StylusPoints[midIdx].ToPoint(),
stroke.StylusPoints[threeQuarterIdx].ToPoint()
};
}
2025-08-03 16:46:33 +08:00
2025-12-06 22:06:28 +08:00
// 计算所有检查点与直线的平均偏差
double totalDeviation = 0;
double maxDeviation = 0;
int validPointCount = 0;
2025-08-03 16:46:33 +08:00
2025-12-06 22:06:28 +08:00
foreach (Point checkPoint in checkPoints)
{
double deviation = DistanceFromLineToPoint(start, end, checkPoint);
totalDeviation += deviation;
maxDeviation = Math.Max(maxDeviation, deviation);
validPointCount++;
}
2025-08-03 16:46:33 +08:00
2025-12-06 22:06:28 +08:00
if (validPointCount > 0)
2025-08-03 16:46:33 +08:00
{
2025-12-06 22:06:28 +08:00
double avgDeviation = totalDeviation / validPointCount;
// 使用相对偏差:偏差与线长的比例,并使用灵敏度进行调整
double quickRelativeThreshold = lineLength * quickThreshold;
// 使用平均偏差和最大偏差的综合判断
2025-12-20 13:56:46 +08:00
double deviationThreshold = Settings.Canvas.HighPrecisionLineStraighten
2025-12-06 22:06:28 +08:00
? Math.Max(avgDeviation, maxDeviation * 0.7) // 高精度模式更严格
: maxDeviation;
// 记录检测到的偏差
Debug.WriteLine($"Deviations: avg={avgDeviation:F2}, max={maxDeviation:F2}, threshold={quickRelativeThreshold:F2}, highPrecision={Settings.Canvas.HighPrecisionLineStraighten}");
if (deviationThreshold > quickRelativeThreshold)
{
return false;
}
2025-06-18 15:10:33 +08:00
}
}
2025-08-03 16:46:33 +08:00
2025-06-18 15:10:33 +08:00
return true;
2025-06-18 13:24:50 +08:00
}
2025-07-29 01:40:35 +08:00
/// <summary>
2025-11-29 22:20:13 +08:00
/// 检查墨迹是否为复杂形状
2025-07-29 01:40:35 +08:00
/// </summary>
/// <param name="stroke">要检查的笔画</param>
/// <returns>如果是复杂形状则返回true,否则返回false</returns>
/// <remarks>
/// 检查墨迹是否为复杂形状的流程:
/// 1. 确保有足够的点来进行分析(至少10个点)
/// 2. 计算直线距离和实际路径长度
/// 3. 如果实际路径长度远大于直线距离(2.5倍以上),说明是复杂形状
/// 4. 检查方向变化次数,如果超过动态阈值,说明是复杂形状
/// </remarks>
2025-07-29 01:40:35 +08:00
private bool IsComplexShape(Stroke stroke)
{
if (stroke.StylusPoints.Count < 10) return false;
Point start = stroke.StylusPoints.First().ToPoint();
Point end = stroke.StylusPoints.Last().ToPoint();
double lineLength = GetDistance(start, end);
// 计算墨迹的实际路径长度
double actualLength = 0;
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point p1 = stroke.StylusPoints[i - 1].ToPoint();
Point p2 = stroke.StylusPoints[i].ToPoint();
actualLength += GetDistance(p1, p2);
}
// 如果实际路径长度远大于直线距离,说明是复杂形状
double complexityRatio = actualLength / Math.Max(lineLength, 1);
if (complexityRatio > 2.5) // 实际路径是直线距离的2.5倍以上
{
Debug.WriteLine($"检测到复杂形状:复杂度比率 = {complexityRatio:F2}");
return true;
}
// 检查方向变化次数
int directionChanges = CountDirectionChanges(stroke);
int maxAllowedChanges = Math.Max(3, stroke.StylusPoints.Count / 20); // 动态阈值
if (directionChanges > maxAllowedChanges)
{
Debug.WriteLine($"检测到复杂形状:方向变化次数 = {directionChanges},阈值 = {maxAllowedChanges}");
return true;
}
return false;
}
/// <summary>
/// 检查是否为明显的曲线(如圆弧、抛物线等)
/// </summary>
/// <param name="stroke">要检查的笔画</param>
/// <returns>如果是明显的曲线则返回true,否则返回false</returns>
/// <remarks>
/// 检查墨迹是否为明显的曲线的流程:
/// 1. 确保有足够的点来进行分析(至少10个点)
/// 2. 计算线条长度
/// 3. 检查曲率一致性,如果一致则认为是明显的曲线
/// 4. 检查中点偏移(对圆弧特别有效):
/// - 计算中点到直线的距离
/// - 如果中点偏移超过线长的15%,且偏移方向一致,可能是圆弧
/// </remarks>
2025-07-29 01:40:35 +08:00
private bool IsObviousCurve(Stroke stroke)
{
if (stroke.StylusPoints.Count < 10) return false;
Point start = stroke.StylusPoints.First().ToPoint();
Point end = stroke.StylusPoints.Last().ToPoint();
double lineLength = GetDistance(start, end);
// 检查曲率一致性
if (HasConsistentCurvature(stroke))
{
Debug.WriteLine("检测到明显曲线:曲率一致");
return true;
}
// 检查中点偏移(对圆弧特别有效)
int midIndex = stroke.StylusPoints.Count / 2;
Point midPoint = stroke.StylusPoints[midIndex].ToPoint();
double midDeviation = DistanceFromLineToPoint(start, end, midPoint);
// 如果中点偏移超过线长的15%,且偏移方向一致,可能是圆弧
if (midDeviation > lineLength * 0.15)
{
// 检查偏移方向的一致性
if (IsConsistentArcDirection(stroke))
{
Debug.WriteLine($"检测到明显曲线:中点偏移 = {midDeviation:F2},线长 = {lineLength:F2}");
return true;
}
}
return false;
}
/// <summary>
/// 计算方向变化次数
/// </summary>
/// <param name="stroke">要检查的笔画</param>
/// <returns>返回方向变化的次数</returns>
/// <remarks>
/// 计算方向变化次数的流程:
/// 1. 确保有足够的点来进行分析(至少3个点)
/// 2. 遍历笔画中的每个点(除了第一个和最后一个)
/// 3. 计算每个点前后线段的角度变化
/// 4. 处理角度跨越问题(超过180度的情况)
/// 5. 如果角度变化超过30度,且与上一次角度变化的差异超过15度,认为是方向变化
/// 6. 返回方向变化的总次数
/// </remarks>
2025-07-29 01:40:35 +08:00
private int CountDirectionChanges(Stroke stroke)
{
if (stroke.StylusPoints.Count < 3) return 0;
int changes = 0;
double lastAngle = 0;
bool hasLastAngle = false;
for (int i = 1; i < stroke.StylusPoints.Count - 1; i++)
{
Point p1 = stroke.StylusPoints[i - 1].ToPoint();
Point p2 = stroke.StylusPoints[i].ToPoint();
Point p3 = stroke.StylusPoints[i + 1].ToPoint();
// 计算角度变化
double angle1 = Math.Atan2(p2.Y - p1.Y, p2.X - p1.X);
double angle2 = Math.Atan2(p3.Y - p2.Y, p3.X - p2.X);
double angleDiff = Math.Abs(angle2 - angle1);
// 处理角度跨越问题
if (angleDiff > Math.PI) angleDiff = 2 * Math.PI - angleDiff;
// 如果角度变化超过30度,认为是方向变化
if (angleDiff > Math.PI / 6) // 30度
{
if (hasLastAngle && Math.Abs(angleDiff - lastAngle) > Math.PI / 12) // 15度
{
changes++;
}
lastAngle = angleDiff;
hasLastAngle = true;
}
}
return changes;
}
/// <summary>
/// 检查曲率是否一致(用于识别圆弧等规则曲线)
/// </summary>
/// <param name="stroke">要检查的笔画</param>
/// <returns>如果曲率一致则返回true,否则返回false</returns>
/// <remarks>
/// 检查曲率是否一致的流程:
/// 1. 确保有足够的点来进行分析(至少15个点)
/// 2. 计算每个点的曲率(使用前后各两个点)
/// 3. 过滤掉无效的曲率值(NaN或无穷大),并取绝对值
/// 4. 确保有足够的有效曲率值(至少5个)
/// 5. 计算曲率的平均值和标准差
/// 6. 如果平均曲率大于0.001且标准差与平均值的比例小于0.5,认为曲率一致
/// </remarks>
2025-07-29 01:40:35 +08:00
private bool HasConsistentCurvature(Stroke stroke)
{
if (stroke.StylusPoints.Count < 15) return false;
List<double> curvatures = new List<double>();
// 计算每个点的曲率
for (int i = 2; i < stroke.StylusPoints.Count - 2; i++)
{
Point p1 = stroke.StylusPoints[i - 2].ToPoint();
Point p2 = stroke.StylusPoints[i].ToPoint();
Point p3 = stroke.StylusPoints[i + 2].ToPoint();
double curvature = CalculateCurvature(p1, p2, p3);
if (!double.IsNaN(curvature) && !double.IsInfinity(curvature))
{
curvatures.Add(Math.Abs(curvature));
}
}
if (curvatures.Count < 5) return false;
// 计算曲率的标准差
double avgCurvature = curvatures.Average();
double variance = curvatures.Select(c => Math.Pow(c - avgCurvature, 2)).Average();
double stdDev = Math.Sqrt(variance);
// 如果曲率变化很小且平均曲率不为零,可能是规则曲线
return avgCurvature > 0.001 && stdDev / avgCurvature < 0.5;
}
/// <summary>
/// 检查圆弧方向是否一致
/// </summary>
private bool IsConsistentArcDirection(Stroke stroke)
{
if (stroke.StylusPoints.Count < 10) return false;
Point start = stroke.StylusPoints.First().ToPoint();
Point end = stroke.StylusPoints.Last().ToPoint();
int positiveDeviations = 0;
int negativeDeviations = 0;
// 检查多个点相对于直线的偏移方向
for (int i = 1; i < stroke.StylusPoints.Count - 1; i += Math.Max(1, stroke.StylusPoints.Count / 10))
{
Point p = stroke.StylusPoints[i].ToPoint();
double signedDistance = SignedDistanceFromLineToPoint(start, end, p);
if (Math.Abs(signedDistance) > 5) // 忽略很小的偏移
{
if (signedDistance > 0) positiveDeviations++;
else negativeDeviations++;
}
}
// 如果大部分点都在直线的同一侧,说明是一致的弧形
int totalSignificantDeviations = positiveDeviations + negativeDeviations;
if (totalSignificantDeviations < 3) return false;
double consistency = Math.Max(positiveDeviations, negativeDeviations) / (double)totalSignificantDeviations;
return consistency > 0.8; // 80%的点在同一侧
}
/// <summary>
/// 计算三点的曲率
/// </summary>
private double CalculateCurvature(Point p1, Point p2, Point p3)
{
// 使用三点计算曲率的公式
double a = GetDistance(p1, p2);
double b = GetDistance(p2, p3);
double c = GetDistance(p1, p3);
if (a == 0 || b == 0 || c == 0) return 0;
// 使用海伦公式计算面积
double s = (a + b + c) / 2;
double areaSquared = s * (s - a) * (s - b) * (s - c);
double area = areaSquared > 0 ? Math.Sqrt(areaSquared) : 0;
2025-07-29 01:40:35 +08:00
// 曲率 = 4 * 面积 / (a * b * c)
return 4 * area / (a * b * c);
}
/// <summary>
/// 计算点到直线的有符号距离
/// </summary>
private double SignedDistanceFromLineToPoint(Point lineStart, Point lineEnd, Point point)
{
// 使用叉积计算有符号距离
double dx = lineEnd.X - lineStart.X;
double dy = lineEnd.Y - lineStart.Y;
double lineLength = Math.Sqrt(dx * dx + dy * dy);
if (lineLength == 0) return 0;
return ((lineEnd.Y - lineStart.Y) * point.X - (lineEnd.X - lineStart.X) * point.Y +
lineEnd.X * lineStart.Y - lineEnd.Y * lineStart.X) / lineLength;
}
/// <summary>
/// 尝试获取直线的端点
/// </summary>
/// <param name="stroke">要分析的笔画</param>
/// <param name="endpoint1">输出参数:直线的第一个端点</param>
/// <param name="endpoint2">输出参数:直线的第二个端点</param>
/// <returns>如果成功获取直线端点则返回true,否则返回false</returns>
/// <remarks>
/// 尝试获取直线端点的流程:
/// 1. 确保笔画有足够的点(至少10个点)
/// 2. 如果启用高精度直线拉直,则对点数进行采样
/// 3. 使用总最小二乘法(TLS/PCA)进行直线拟合
/// 4. 计算中心点和协方差矩阵
/// 5. 计算特征值和特征向量,确定直线方向
/// 6. 计算解释方差比例(拟合优度)
/// 7. 计算所有点在直线方向上的投影,找到最小和最大投影值
/// 8. 根据投影值计算端点坐标
/// 9. 根据解释方差比例判断是否为直线
/// </remarks>
2025-11-29 22:20:13 +08:00
private bool TryGetStraightLineEndpoints(Stroke stroke, out Point endpoint1, out Point endpoint2)
2025-08-03 16:46:33 +08:00
{
2025-11-29 22:20:13 +08:00
endpoint1 = new Point();
endpoint2 = new Point();
2025-07-29 01:40:35 +08:00
2025-11-29 22:20:13 +08:00
var points = stroke.StylusPoints.Select(p => p.ToPoint()).ToList();
if (points.Count < 10)
2025-07-29 01:40:35 +08:00
{
return false;
}
2025-12-06 22:06:28 +08:00
List<Point> workingPoints = points;
if (Settings.Canvas.HighPrecisionLineStraighten)
{
workingPoints = SamplePointsByDistance(points, 10.0);
Debug.WriteLine($"高精度模式:原始点数={points.Count}, 采样后点数={workingPoints.Count}");
}
2025-11-29 22:20:13 +08:00
// 使用总最小二乘法(TLS/PCA)进行直线拟合
2025-12-06 22:06:28 +08:00
int n = workingPoints.Count - 8;
if (n < 1)
{
// 如果采样后点数太少,回退到原始方法
n = points.Count - 8;
workingPoints = points;
}
2025-11-29 22:20:13 +08:00
List<Point> filteredPoints = new List<Point>();
2025-07-29 01:40:35 +08:00
2025-11-29 22:20:13 +08:00
// 收集过滤后的点(跳过前 4 个和后 4 个点,用于计算直线方向)
2025-12-06 22:06:28 +08:00
int skipCount = Math.Min(4, n / 2); // 确保跳过数量不超过一半
for (int i = skipCount; i < n + skipCount && i < workingPoints.Count; i++)
2025-07-29 01:40:35 +08:00
{
2025-12-06 22:06:28 +08:00
filteredPoints.Add(workingPoints[i]);
2025-06-18 15:10:33 +08:00
}
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 计算中心点(使用过滤后的点)
double centerX = 0, centerY = 0;
foreach (Point p in filteredPoints)
2025-08-03 16:46:33 +08:00
{
2025-11-29 22:20:13 +08:00
centerX += p.X;
centerY += p.Y;
}
centerX /= filteredPoints.Count;
centerY /= filteredPoints.Count;
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 计算协方差矩阵(使用过滤后的点)
double covXX = 0, covYY = 0, covXY = 0;
foreach (Point p in filteredPoints)
{
double dx = p.X - centerX;
double dy = p.Y - centerY;
covXX += dx * dx;
covYY += dy * dy;
covXY += dx * dy;
}
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 计算特征值和特征向量
double trace = covXX + covYY;
double determinant = covXX * covYY - covXY * covXY;
double discriminantSquared = trace * trace - 4 * determinant;
double discriminant = discriminantSquared > 0 ? Math.Sqrt(discriminantSquared) : 0;
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
double eigenvalue1 = (trace + discriminant) / 2;
double eigenvalue2 = (trace - discriminant) / 2;
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 最大特征值对应的特征向量即为直线方向
double directionX, directionY;
if (Math.Abs(covXY) > 1e-10)
{
directionX = covXY;
directionY = eigenvalue1 - covXX;
// 归一化
double length = Math.Sqrt(directionX * directionX + directionY * directionY);
2025-12-31 16:57:03 +08:00
if (length > 1e-10)
{
directionX /= length;
directionY /= length;
}
else
{
// 如果归一化失败,使用起点和终点计算方向
Point start = points.First();
Point end = points.Last();
double dx = end.X - start.X;
double dy = end.Y - start.Y;
double lineLength = Math.Sqrt(dx * dx + dy * dy);
if (lineLength > 1e-10)
{
directionX = dx / lineLength;
directionY = dy / lineLength;
}
else
{
directionX = (covXX >= covYY) ? 1 : 0;
directionY = (covXX >= covYY) ? 0 : 1;
}
}
2025-08-03 16:46:33 +08:00
}
else
{
2025-12-31 16:57:03 +08:00
Point start = points.First();
Point end = points.Last();
double dx = end.X - start.X;
double dy = end.Y - start.Y;
double lineLength = Math.Sqrt(dx * dx + dy * dy);
2026-03-03 16:04:20 +08:00
2025-12-31 16:57:03 +08:00
if (lineLength > 1e-10)
{
directionX = dx / lineLength;
directionY = dy / lineLength;
}
else
{
if (Math.Abs(eigenvalue1 - covXX) < Math.Abs(eigenvalue1 - covYY))
{
// 主要方向是 X 轴方向
directionX = 1;
directionY = 0;
}
else
{
// 主要方向是 Y 轴方向
directionX = 0;
directionY = 1;
}
}
2025-06-18 15:10:33 +08:00
}
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 计算解释方差比例(拟合优度)
double totalVariance = eigenvalue1 + eigenvalue2;
double explainedVarianceRatio = (totalVariance > 1e-10) ?
Math.Max(eigenvalue1, eigenvalue2) / totalVariance : 1d;
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 使用所有点计算端点
double minProjection = double.MaxValue;
double maxProjection = double.MinValue;
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 计算所有点在直线方向上的投影
2025-12-06 22:06:28 +08:00
List<Point> pointsForProjection = Settings.Canvas.HighPrecisionLineStraighten ? workingPoints : points;
foreach (Point p in pointsForProjection)
2025-08-03 16:46:33 +08:00
{
2025-11-29 22:20:13 +08:00
// 相对于过滤点中心的投影
double projection = (p.X - centerX) * directionX + (p.Y - centerY) * directionY;
minProjection = Math.Min(minProjection, projection);
maxProjection = Math.Max(maxProjection, projection);
2025-07-28 14:40:44 +08:00
}
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 计算端点坐标
endpoint1 = new Point(
centerX + minProjection * directionX,
centerY + minProjection * directionY
);
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
endpoint2 = new Point(
centerX + maxProjection * directionX,
centerY + maxProjection * directionY
);
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 使用解释方差比例作为判断条件
double threshold = 0.998 + Settings.InkToShape.LineNormalizationThreshold / 500;
return explainedVarianceRatio > threshold;
}
2025-10-03 17:08:46 +08:00
/// <summary>
/// 确定笔画是否应该被拉成直线
/// </summary>
/// <param name="stroke">要分析的笔画</param>
/// <returns>如果笔画应该被拉成直线则返回true,否则返回false</returns>
/// <remarks>
/// 确定笔画是否应该被拉成直线的流程:
/// 1. 计算线条长度和分辨率自适应阈值
/// 2. 如果线条太短,不进行拉直处理
/// 3. 检查线条复杂度,如果是复杂形状,不进行拉直处理
/// 4. 尝试获取直线端点,判断是否满足直线条件
/// 5. 根据判断结果返回相应的布尔值
/// </remarks>
2025-11-29 22:20:13 +08:00
private bool ShouldStraightenLine(Stroke stroke)
{
// 分辨率自适应阈值
Point start = stroke.StylusPoints.First().ToPoint();
Point end = stroke.StylusPoints.Last().ToPoint();
double lineLength = GetDistance(start, end);
double adaptiveThreshold = Settings.Canvas.AutoStraightenLineThreshold * GetResolutionScale();
2025-12-20 13:56:46 +08:00
2025-11-29 22:20:13 +08:00
// 如果线条太短,不进行拉直处理
if (lineLength < adaptiveThreshold)
2025-08-03 16:46:33 +08:00
{
2025-11-29 22:20:13 +08:00
Debug.WriteLine($"线条太短: {lineLength} < {adaptiveThreshold}");
2025-07-28 14:40:44 +08:00
return false;
}
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
// 检查复杂度
if (IsComplexShape(stroke))
2025-08-03 16:46:33 +08:00
{
2025-11-29 22:20:13 +08:00
Debug.WriteLine("拒绝拉直:检测到复杂形状");
2025-07-28 14:40:44 +08:00
return false;
}
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
Point endpoint1, endpoint2;
bool shouldStraighten = TryGetStraightLineEndpoints(stroke, out endpoint1, out endpoint2);
2025-12-20 13:56:46 +08:00
2025-11-29 22:20:13 +08:00
if (shouldStraighten)
2025-08-03 16:46:33 +08:00
{
2025-11-29 22:20:13 +08:00
Debug.WriteLine($"接受拉直:判断为直线,解释方差比例满足阈值");
}
else
{
Debug.WriteLine($"拒绝拉直:判断不满足直线条件");
2025-06-18 13:24:50 +08:00
}
2025-08-03 16:46:33 +08:00
2025-11-29 22:20:13 +08:00
return shouldStraighten;
2025-06-18 13:24:50 +08:00
}
2025-07-29 01:40:35 +08:00
/// <summary>
/// 计算墨迹的直线度评分(0-1,1表示完美直线)
/// </summary>
/// <param name="stroke">要分析的笔画</param>
/// <returns>返回直线度评分,范围为0到11表示完美直线</returns>
/// <remarks>
/// 计算墨迹直线度评分的流程:
/// 1. 确保笔画有足够的点(至少3个点)
/// 2. 计算线条长度
/// 3. 计算偏差评分(基于点到直线的距离):
/// - 计算所有点到直线的平均偏差和最大偏差
/// - 根据偏差计算评分
/// 4. 计算方向一致性评分:
/// - 计算每个线段与目标方向的角度差
/// - 将角度差转换为评分
/// 5. 计算路径效率评分:
/// - 计算实际路径长度与直线距离的比例
/// 6. 计算端点连接度评分(默认满分)
/// 7. 综合评分(加权平均):
/// - 偏差评分:40%
/// - 方向一致性评分:30%
/// - 路径效率评分:20%
/// - 端点连接度评分:10%
/// 8. 返回最终评分,确保在0到1之间
/// </remarks>
2025-07-29 01:40:35 +08:00
private double CalculateStraightnessScore(Stroke stroke)
{
if (stroke.StylusPoints.Count < 3) return 0;
Point start = stroke.StylusPoints.First().ToPoint();
Point end = stroke.StylusPoints.Last().ToPoint();
double lineLength = GetDistance(start, end);
if (lineLength == 0) return 0;
// 1. 计算偏差评分(基于点到直线的距离)
double totalDeviation = 0;
double maxDeviation = 0;
int pointCount = 0;
foreach (StylusPoint sp in stroke.StylusPoints)
{
Point p = sp.ToPoint();
double deviation = DistanceFromLineToPoint(start, end, p);
totalDeviation += deviation;
maxDeviation = Math.Max(maxDeviation, deviation);
pointCount++;
}
double avgDeviation = totalDeviation / pointCount;
// 偏差评分:基于平均偏差和最大偏差
double deviationScore = Math.Max(0, 1 - (avgDeviation / (lineLength * 0.05)) - (maxDeviation / (lineLength * 0.1)));
// 2. 计算方向一致性评分
double directionScore = CalculateDirectionConsistency(stroke);
// 3. 计算路径效率评分(实际路径长度 vs 直线距离)
double actualLength = 0;
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point p1 = stroke.StylusPoints[i - 1].ToPoint();
Point p2 = stroke.StylusPoints[i].ToPoint();
actualLength += GetDistance(p1, p2);
}
double efficiencyScore = Math.Max(0, Math.Min(1, lineLength / actualLength));
// 4. 计算端点连接度评分(起点到终点的直接性)
double endpointScore = 1.0; // 默认满分,因为我们已经有了起点和终点
// 综合评分(加权平均)
double finalScore = (deviationScore * 0.4 + directionScore * 0.3 + efficiencyScore * 0.2 + endpointScore * 0.1);
Debug.WriteLine($"直线度评分详情: 偏差={deviationScore:F3}, 方向={directionScore:F3}, 效率={efficiencyScore:F3}, 综合={finalScore:F3}");
return Math.Max(0, Math.Min(1, finalScore));
}
/// <summary>
/// 计算方向一致性评分
/// </summary>
/// <param name="stroke">要分析的笔画</param>
/// <returns>返回方向一致性评分,范围为0到1,1表示方向完全一致</returns>
/// <remarks>
/// 计算方向一致性评分的流程:
/// 1. 确保笔画有足够的点(至少5个点)
/// 2. 计算目标方向(从起点到终点的方向)
/// 3. 计算每个线段与目标方向的角度差:
/// - 遍历笔画中的每个线段
/// - 忽略太短的线段(长度小于2)
/// - 计算线段的角度
/// - 计算与目标方向的角度差
/// - 处理角度跨越问题(超过180度的情况)
/// 4. 计算平均角度差
/// 5. 将角度差转换为评分(0-1):
/// - 0度差 = 1分
/// - 90度差 = 0分
/// 6. 返回方向一致性评分
/// </remarks>
2025-07-29 01:40:35 +08:00
private double CalculateDirectionConsistency(Stroke stroke)
{
if (stroke.StylusPoints.Count < 5) return 1.0;
Point start = stroke.StylusPoints.First().ToPoint();
Point end = stroke.StylusPoints.Last().ToPoint();
// 目标方向
double targetAngle = Math.Atan2(end.Y - start.Y, end.X - start.X);
double totalAngleDifference = 0;
int segmentCount = 0;
// 计算每个线段与目标方向的角度差
for (int i = 1; i < stroke.StylusPoints.Count; i++)
{
Point p1 = stroke.StylusPoints[i - 1].ToPoint();
Point p2 = stroke.StylusPoints[i].ToPoint();
double segmentLength = GetDistance(p1, p2);
if (segmentLength < 2) continue; // 忽略太短的线段
double segmentAngle = Math.Atan2(p2.Y - p1.Y, p2.X - p1.X);
double angleDiff = Math.Abs(segmentAngle - targetAngle);
// 处理角度跨越问题
if (angleDiff > Math.PI) angleDiff = 2 * Math.PI - angleDiff;
totalAngleDifference += angleDiff;
segmentCount++;
}
if (segmentCount == 0) return 1.0;
double avgAngleDifference = totalAngleDifference / segmentCount;
// 将角度差转换为评分(0-1
// 0度差 = 1分,90度差 = 0分
double directionScore = Math.Max(0, 1 - (avgAngleDifference / (Math.PI / 2)));
return directionScore;
}
2025-08-03 16:46:33 +08:00
/// <summary>
/// 在两点之间创建直线笔画
/// </summary>
/// <param name="start">直线的起始点</param>
/// <param name="end">直线的结束点</param>
/// <returns>返回包含直线点集的StylusPointCollection</returns>
/// <remarks>
/// 在两点之间创建直线笔画的流程:
/// 1. 根据是否启用压感触屏模式决定如何设置压感:
/// - 如果未启用压感触屏模式、禁用压感、启用无压感矩形或使用钢笔类型1,则使用均匀粗细(压感值0.5)
/// - 否则,创建带有压感变化的直线:
/// - 计算中点
/// - 从起点到中点:压感从0.4渐变到0.8
/// - 从中点到终点:压感从0.8渐变到0.4
/// 2. 使用GeneratePointsBetween方法生成点集
/// 3. 返回生成的点集
/// </remarks>
2025-08-03 16:46:33 +08:00
private StylusPointCollection CreateStraightLine(Point start, Point end)
{
2025-06-18 13:24:50 +08:00
StylusPointCollection points = new StylusPointCollection();
2025-08-03 16:46:33 +08:00
2025-06-18 18:31:03 +08:00
// 根据是否启用压感触屏模式决定如何设置压感
// 如果未启用压感触屏模式,则使用均匀粗细
2025-08-03 16:46:33 +08:00
if (!Settings.Canvas.EnablePressureTouchMode || Settings.Canvas.DisablePressure ||
Settings.InkToShape.IsInkToShapeNoFakePressureRectangle || penType == 1)
{
2025-12-27 18:22:00 +08:00
var linePoints = GeneratePointsBetween(start, end, 0.5f, 0.5f, 8.0);
foreach (var pt in linePoints)
2025-08-03 16:46:33 +08:00
{
2025-12-27 18:22:00 +08:00
points.Add(pt);
2025-06-18 18:31:03 +08:00
}
2025-08-03 16:46:33 +08:00
}
else
{
2025-06-18 13:24:50 +08:00
Point midPoint = new Point((start.X + end.X) / 2, (start.Y + end.Y) / 2);
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
var startToMid = GeneratePointsBetween(start, midPoint, 0.4f, 0.8f, 8.0);
foreach (var pt in startToMid)
{
points.Add(pt);
}
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
var midToEnd = GeneratePointsBetween(midPoint, end, 0.8f, 0.4f, 8.0);
for (int i = 1; i < midToEnd.Count; i++)
{
points.Add(midToEnd[i]);
}
2025-06-18 13:24:50 +08:00
}
2025-08-03 16:46:33 +08:00
2025-06-18 13:24:50 +08:00
return points;
}
2025-08-03 16:46:33 +08:00
2025-12-06 22:06:28 +08:00
/// <summary>
/// 根据距离对点数进行采样
2025-12-06 22:06:28 +08:00
/// </summary>
/// <param name="points">原始点列表</param>
/// <param name="sampleInterval">采样间隔,默认为10.0</param>
/// <returns>返回采样后的点列表</returns>
/// <remarks>
/// 根据距离对点数进行采样的流程:
/// 1. 确保原始点列表不为空且至少有2个点
/// 2. 总是包含起点
/// 3. 遍历原始点列表,计算累积距离:
/// - 当累积距离达到采样间隔时,添加当前点
/// - 重置累积距离
/// 4. 总是包含终点(如果还没有包含)
/// 5. 返回采样后的点列表
/// </remarks>
2025-12-06 22:06:28 +08:00
private List<Point> SamplePointsByDistance(List<Point> points, double sampleInterval = 10.0)
{
if (points == null || points.Count < 2)
return points;
List<Point> sampledPoints = new List<Point>();
sampledPoints.Add(points[0]); // 总是包含起点
double accumulatedDistance = 0;
Point lastSampledPoint = points[0];
for (int i = 1; i < points.Count; i++)
{
double segmentDistance = GetDistance(lastSampledPoint, points[i]);
accumulatedDistance += segmentDistance;
// 当累积距离达到采样间隔时,添加当前点
if (accumulatedDistance >= sampleInterval)
{
sampledPoints.Add(points[i]);
lastSampledPoint = points[i];
accumulatedDistance = 0; // 重置累积距离
}
}
// 总是包含终点(如果还没有包含)
if (sampledPoints.Count == 0 || GetDistance(sampledPoints.Last(), points.Last()) > 1.0)
{
sampledPoints.Add(points.Last());
}
return sampledPoints;
}
/// <summary>
/// 计算点到直线的距离
/// </summary>
/// <param name="lineStart">直线的起始点</param>
/// <param name="lineEnd">直线的结束点</param>
/// <param name="point">要计算距离的点</param>
/// <returns>返回点到直线的距离</returns>
/// <remarks>
/// 计算点到直线距离的流程:
/// 1. 计算直线的长度
/// 2. 如果直线长度为0(即两个点重合),则返回点到该点的距离
/// 3. 否则,使用叉积计算点到直线的垂直距离
/// 4. 返回计算得到的距离
/// </remarks>
2025-08-03 16:46:33 +08:00
private double DistanceFromLineToPoint(Point lineStart, Point lineEnd, Point point)
{
2025-06-18 13:24:50 +08:00
// Calculate distance from point to line defined by lineStart and lineEnd
double lineLength = GetDistance(lineStart, lineEnd);
if (lineLength == 0) return GetDistance(point, lineStart);
2025-08-03 16:46:33 +08:00
2025-06-18 13:24:50 +08:00
// Calculate the cross product to get the perpendicular distance
2025-08-03 16:46:33 +08:00
double distance = Math.Abs((lineEnd.Y - lineStart.Y) * point.X -
(lineEnd.X - lineStart.X) * point.Y +
2025-06-18 13:24:50 +08:00
lineEnd.X * lineStart.Y - lineEnd.Y * lineStart.X) / lineLength;
return distance;
}
2025-08-03 16:46:33 +08:00
2025-12-13 20:32:43 +08:00
/// <summary>
/// 判断一个 stroke 是否是直线(排除虚线和点线)
/// </summary>
/// <param name="stroke">要检查的 stroke</param>
/// <returns>如果是直线返回 true,否则返回 false</returns>
/// <remarks>
/// 判断一个 stroke 是否是直线的流程:
/// 1. 检查 stroke 是否为空或没有点,如果是则返回 false
/// 2. 检查点的数量:
/// - 如果只有1个点,返回 false
/// - 如果有2个点:
/// - 计算两点之间的距离
/// - 如果距离小于10,返回 false
/// - 否则返回 true
/// - 如果有3个点:
/// - 计算第一个点和第三个点之间的距离
/// - 如果距离小于10,返回 false
/// - 计算第二个点到由第一个点和第三个点组成的直线的距离
/// - 如果距离相对于线段长度很小(小于1%),认为是直线,返回 true
/// - 否则返回 false
/// - 如果点的数量大于3,返回 false
/// </remarks>
2025-12-13 20:32:43 +08:00
private bool IsStraightLine(Stroke stroke)
{
if (stroke == null || stroke.StylusPoints.Count == 0)
return false;
int pointCount = stroke.StylusPoints.Count;
if (pointCount == 1)
return false;
// 最简单的直线:只有2个点
if (pointCount == 2)
{
Point p1 = stroke.StylusPoints[0].ToPoint();
Point p2 = stroke.StylusPoints[1].ToPoint();
double lineLength = GetDistance(p1, p2);
2025-12-20 13:56:46 +08:00
2025-12-13 20:32:43 +08:00
if (lineLength < 10)
return false;
return true;
}
if (pointCount > 3)
return false;
// 对于3个点的情况,检查它们是否基本在一条直线上
if (pointCount == 3)
{
Point p1 = stroke.StylusPoints[0].ToPoint();
Point p2 = stroke.StylusPoints[1].ToPoint();
Point p3 = stroke.StylusPoints[2].ToPoint();
double totalLength = GetDistance(p1, p3);
if (totalLength < 10)
return false;
// 计算点到直线的距离
// 使用 p1 和 p3 作为直线端点,检查 p2 是否在这条直线上
double distance = DistanceFromLineToPoint(p1, p3, p2);
// 如果点到直线的距离相对于线段长度很小,认为是直线
// 使用相对误差阈值(比如 1%
if (totalLength > 0 && distance / totalLength < 0.01)
return true;
return false;
}
return false;
}
2026-02-22 11:42:03 +08:00
private bool IsValidStraightLineSnapTarget(Stroke stroke)
{
if (stroke == null || stroke.StylusPoints.Count == 0)
return false;
if (stroke.StylusPoints.Count == 1)
return false;
if (stroke.StylusPoints.Count <= 3)
{
if (!IsStraightLine(stroke))
return false;
double len = GetDistance(stroke.StylusPoints.First().ToPoint(), stroke.StylusPoints.Last().ToPoint());
if (len < 20)
return false;
return true;
}
return IsPotentialStraightLine(stroke);
}
/// <summary>
/// 尝试将直线端点吸附到现有笔画的端点
/// </summary>
/// <param name="start">直线的起始点</param>
/// <param name="end">直线的结束点</param>
/// <returns>返回吸附后的端点数组,如果没有发生吸附则返回null</returns>
/// <remarks>
/// 尝试将直线端点吸附到现有笔画端点的流程:
/// 1. 检查是否启用了线段端点吸附功能,如果没有启用则返回null
/// 2. 初始化吸附状态和吸附后的点
/// 3. 获取设置中的吸附距离阈值
/// 4. 遍历画布中的所有笔画:
/// - 跳过没有点的笔画
/// - 只对直线进行端点吸附,跳过虚线和点线
/// - 获取笔画的起点和终点
/// - 检查起点是否应该吸附到现有笔画的端点
/// - 检查终点是否应该吸附到现有笔画的端点
/// - 如果两个端点都已经吸附,结束遍历
/// 5. 如果发生了吸附,返回吸附后的端点数组,否则返回null
/// </remarks>
2025-08-03 16:46:33 +08:00
private Point[] GetSnappedEndpoints(Point start, Point end)
{
2025-06-19 14:30:24 +08:00
if (!Settings.Canvas.LineEndpointSnapping)
return null;
2025-08-03 16:46:33 +08:00
2025-06-18 13:24:50 +08:00
bool startSnapped = false;
bool endSnapped = false;
Point snappedStart = start;
Point snappedEnd = end;
2025-08-03 16:46:33 +08:00
2025-06-19 14:30:24 +08:00
// 使用设置中的吸附距离阈值
double snapThreshold = Settings.Canvas.LineEndpointSnappingThreshold;
2025-08-03 16:46:33 +08:00
2025-06-18 13:24:50 +08:00
// Check all strokes in canvas for potential snap points
2025-08-03 16:46:33 +08:00
foreach (Stroke stroke in inkCanvas.Strokes)
{
2025-06-18 13:24:50 +08:00
if (stroke.StylusPoints.Count == 0) continue;
2025-08-03 16:46:33 +08:00
2025-12-13 20:32:43 +08:00
// 只对直线进行端点吸附,跳过虚线和点线
2026-02-22 11:42:03 +08:00
if (!IsValidStraightLineSnapTarget(stroke))
2025-12-13 20:32:43 +08:00
continue;
2025-06-18 13:24:50 +08:00
// Get stroke endpoints
Point strokeStart = stroke.StylusPoints.First().ToPoint();
Point strokeEnd = stroke.StylusPoints.Last().ToPoint();
2025-08-03 16:46:33 +08:00
2025-06-18 13:24:50 +08:00
// Check if start point should snap to an endpoint
2025-08-03 16:46:33 +08:00
if (!startSnapped)
{
if (GetDistance(start, strokeStart) < snapThreshold)
{
2025-06-18 13:24:50 +08:00
snappedStart = strokeStart;
startSnapped = true;
2025-08-03 16:46:33 +08:00
}
else if (GetDistance(start, strokeEnd) < snapThreshold)
{
2025-06-18 13:24:50 +08:00
snappedStart = strokeEnd;
startSnapped = true;
}
}
2025-08-03 16:46:33 +08:00
2025-06-18 13:24:50 +08:00
// Check if end point should snap to an endpoint
2025-08-03 16:46:33 +08:00
if (!endSnapped)
{
if (GetDistance(end, strokeStart) < snapThreshold)
{
2025-06-18 13:24:50 +08:00
snappedEnd = strokeStart;
endSnapped = true;
2025-08-03 16:46:33 +08:00
}
else if (GetDistance(end, strokeEnd) < snapThreshold)
{
2025-06-18 13:24:50 +08:00
snappedEnd = strokeEnd;
endSnapped = true;
}
}
2025-08-03 16:46:33 +08:00
2025-06-18 13:24:50 +08:00
// If both endpoints are snapped, we're done
if (startSnapped && endSnapped) break;
}
2025-08-03 16:46:33 +08:00
2025-06-18 13:24:50 +08:00
// Return snapped points if any snapping occurred
2025-08-03 16:46:33 +08:00
if (startSnapped || endSnapped)
{
2025-07-28 14:40:44 +08:00
return new[] { snappedStart, snappedEnd };
2025-06-18 13:24:50 +08:00
}
2025-08-03 16:46:33 +08:00
2025-06-18 13:24:50 +08:00
return null;
}
/// <summary>
/// 设置新的笔画备份
/// </summary>
/// <remarks>
/// 设置新的笔画备份的流程:
/// 1. 克隆当前墨水画布的笔画集合
/// 2. 获取当前白板索引
/// 3. 如果当前模式为0,则将白板索引设置为0
/// 4. 将克隆的笔画集合存储到strokeCollections中对应索引的位置
/// </remarks>
2025-08-03 16:46:33 +08:00
private void SetNewBackupOfStroke()
{
2025-05-25 09:29:48 +08:00
lastTouchDownStrokeCollection = inkCanvas.Strokes.Clone();
var whiteboardIndex = CurrentWhiteboardIndex;
if (currentMode == 0) whiteboardIndex = 0;
strokeCollections[whiteboardIndex] = lastTouchDownStrokeCollection;
}
/// <summary>
/// 计算两点之间的距离
/// </summary>
/// <param name="point1">第一个点</param>
/// <param name="point2">第二个点</param>
/// <returns>返回两点之间的距离</returns>
/// <remarks>
/// 使用欧几里得距离公式计算两点之间的距离:
/// distance = √[(x2 - x1)² + (y2 - y1)²]
/// </remarks>
2025-08-03 16:46:33 +08:00
public double GetDistance(Point point1, Point point2)
{
2025-05-25 09:29:48 +08:00
return Math.Sqrt((point1.X - point2.X) * (point1.X - point2.X) +
(point1.Y - point2.Y) * (point1.Y - point2.Y));
}
/// <summary>
/// 计算点的速度
/// </summary>
/// <param name="point1">第一个点</param>
/// <param name="point2">第二个点(当前点)</param>
/// <param name="point3">第三个点</param>
/// <returns>返回点的速度</returns>
/// <remarks>
/// 计算点速度的流程:
/// 1. 计算第一个点到第二个点的距离
/// 2. 计算第三个点到第二个点的距离
/// 3. 将两个距离相加
/// 4. 除以20,得到速度值
/// </remarks>
2025-08-03 16:46:33 +08:00
public double GetPointSpeed(Point point1, Point point2, Point point3)
{
2025-05-25 09:29:48 +08:00
return (Math.Sqrt((point1.X - point2.X) * (point1.X - point2.X) +
(point1.Y - point2.Y) * (point1.Y - point2.Y))
+ Math.Sqrt((point3.X - point2.X) * (point3.X - point2.X) +
(point3.Y - point2.Y) * (point3.Y - point2.Y)))
/ 20;
}
2026-04-04 22:56:34 +08:00
private static float RateBasedPressureFactorFromPointSpeed(double speed)
{
if (speed >= 0.25)
return (float)(0.5 - 0.3 * (Math.Min(speed, 1.5) - 0.3) / 1.2);
if (speed >= 0.05)
return 0.5f;
return (float)(0.5 + 0.4 * (0.05 - speed) / 0.05);
}
private static float RealtimeBrushTipMixRatePressureFromSpeed(double speed)
{
if (speed < 0) speed = 0;
const double slowRef = 0.012;
const double fastRef = 0.5;
var t = (speed - slowRef) / (fastRef - slowRef);
if (t < 0) t = 0;
else if (t > 1) t = 1;
t = t * t * (3.0 - 2.0 * t);
const double pThick = 0.9;
const double pThin = 0.22;
var p = pThick + (pThin - pThick) * t;
return (float)Math.Max(0.08, Math.Min(1.0, p));
}
private static bool IsStrokePressureApproximatelyConstant(StylusPointCollection pts, out float meanPf)
{
meanPf = 0.5f;
if (pts == null || pts.Count == 0) return true;
double sum = 0;
foreach (StylusPoint p in pts) sum += p.PressureFactor;
meanPf = (float)(sum / pts.Count);
const float tol = 0.04f;
foreach (StylusPoint p in pts)
{
if (Math.Abs(p.PressureFactor - meanPf) > tol)
return false;
}
return true;
}
2026-04-30 17:51:43 +08:00
private void ReplaceStrokesSafely(StrokeCollection strokesToRemove, Stroke replacementStroke, Stroke fallbackOriginalStroke = null)
{
if (replacementStroke == null) return;
try
{
SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition;
if (strokesToRemove != null && strokesToRemove.Count > 0)
inkCanvas.Strokes.Remove(strokesToRemove);
if (fallbackOriginalStroke != null && inkCanvas.Strokes.Contains(fallbackOriginalStroke))
inkCanvas.Strokes.Remove(fallbackOriginalStroke);
if (!inkCanvas.Strokes.Contains(replacementStroke))
inkCanvas.Strokes.Add(replacementStroke);
}
finally
{
_currentCommitType = CommitReason.UserInput;
}
}
private void ReplaceStrokesSafely(StrokeCollection strokesToRemove, StrokeCollection replacementStrokes, Stroke fallbackOriginalStroke = null)
{
if (replacementStrokes == null || replacementStrokes.Count == 0) return;
try
{
SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition;
if (strokesToRemove != null && strokesToRemove.Count > 0)
inkCanvas.Strokes.Remove(strokesToRemove);
if (fallbackOriginalStroke != null && inkCanvas.Strokes.Contains(fallbackOriginalStroke))
inkCanvas.Strokes.Remove(fallbackOriginalStroke);
inkCanvas.Strokes.Add(replacementStrokes);
}
finally
{
_currentCommitType = CommitReason.UserInput;
}
}
2026-03-28 16:59:02 +08:00
/// <summary>
2026-04-04 22:56:34 +08:00
/// 将沿线速度映射为压感并与硬件压感混合,快写略细、慢写略粗;在落笔时(及手写笔移动时由调用方)统一施加。
2026-04-05 18:52:19 +08:00
/// 无压感设备上系统可能将 <see cref="DrawingAttributes.IgnorePressure"/> 置为 true,此处强制关闭以便粗细随合成压感变化。
/// 若 <see cref="Settings.Canvas.DisablePressure"/> 为 true,本方法直接返回且不修改 IgnorePressure。
2026-03-28 16:59:02 +08:00
/// </summary>
private void ApplyVelocityBrushTipFromSpeed(Stroke stroke)
{
try
{
2026-04-05 18:52:19 +08:00
if (Settings.Canvas.DisablePressure)
return;
2026-03-28 16:59:02 +08:00
var mix = Settings.Canvas.VelocityBrushTipMix;
if (mix <= 0 || stroke == null) return;
if (mix > 1) mix = 1;
2026-04-04 22:16:37 +08:00
if (stroke.DrawingAttributes != null)
stroke.DrawingAttributes.IgnorePressure = false;
2026-03-28 16:59:02 +08:00
var pts = stroke.StylusPoints;
if (pts.Count < 3) return;
2026-04-04 22:56:34 +08:00
var effectiveMix = (float)mix;
if (IsStrokePressureApproximatelyConstant(pts, out _))
effectiveMix = Math.Max(effectiveMix, 0.78f);
2026-03-28 16:59:02 +08:00
var n = pts.Count - 1;
var stylusPoints = new StylusPointCollection();
for (var i = 0; i <= n; i++)
{
var speed = GetPointSpeed(
pts[Math.Max(i - 1, 0)].ToPoint(),
pts[i].ToPoint(),
pts[Math.Min(i + 1, n)].ToPoint());
2026-04-04 22:56:34 +08:00
var speedPressure = RealtimeBrushTipMixRatePressureFromSpeed(speed);
2026-03-28 16:59:02 +08:00
var basePf = pts[i].PressureFactor;
2026-04-04 22:56:34 +08:00
var blended = (1.0f - effectiveMix) * basePf + effectiveMix * speedPressure;
2026-03-28 16:59:02 +08:00
blended = (float)Math.Max(0.08, Math.Min(1.0, blended));
var p = new StylusPoint(pts[i].X, pts[i].Y, blended);
stylusPoints.Add(p);
}
stroke.StylusPoints = stylusPoints;
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
2025-08-03 16:46:33 +08:00
public Point[] FixPointsDirection(Point p1, Point p2)
{
double deltaY = Math.Abs(p1.Y - p2.Y);
double deltaX = Math.Abs(p1.X - p2.X);
2026-03-03 16:04:20 +08:00
if (deltaY < 1e-10 || deltaX / deltaY > 8)
2025-08-03 16:46:33 +08:00
{
2025-05-25 09:29:48 +08:00
//水平
var x = deltaY / 2;
2025-08-03 16:46:33 +08:00
if (p1.Y > p2.Y)
{
2025-05-25 09:29:48 +08:00
p1.Y -= x;
p2.Y += x;
}
2025-08-03 16:46:33 +08:00
else
{
2025-05-25 09:29:48 +08:00
p1.Y += x;
p2.Y -= x;
}
}
else if (deltaX < 1e-10 || deltaY / deltaX > 8)
2025-08-03 16:46:33 +08:00
{
2025-05-25 09:29:48 +08:00
//垂直
var x = deltaX / 2;
2025-08-03 16:46:33 +08:00
if (p1.X > p2.X)
{
2025-05-25 09:29:48 +08:00
p1.X -= x;
p2.X += x;
}
2025-08-03 16:46:33 +08:00
else
{
2025-05-25 09:29:48 +08:00
p1.X += x;
p2.X -= x;
}
}
return new Point[2] { p1, p2 };
}
2026-04-18 17:10:27 +08:00
/// <summary>
/// 用点集拟合出 IACore 风格椭圆参数(中心/长短半轴/方向/四个端点)。
/// 解决 WinRT 返回热点顺序不稳定导致椭圆纠正角度翻转的问题。
/// </summary>
private static bool TryEstimateEllipseParamsFromStrokes(
StrokeCollection strokes,
out Point centroid,
out double a,
out double b,
out double thetaRad,
out Point major0,
out Point major1,
out Point minor0,
out Point minor1)
{
centroid = default;
a = b = 0;
thetaRad = 0;
major0 = major1 = minor0 = minor1 = default;
if (strokes == null || strokes.Count == 0) return false;
var pts = new List<Point>(256);
foreach (var s in strokes)
{
if (s?.StylusPoints == null) continue;
foreach (var sp in s.StylusPoints)
pts.Add(sp.ToPoint());
}
if (pts.Count < 12) return false;
double mx = 0, my = 0;
for (int i = 0; i < pts.Count; i++)
{
mx += pts[i].X;
my += pts[i].Y;
}
mx /= pts.Count;
my /= pts.Count;
centroid = new Point(mx, my);
double sxx = 0, syy = 0, sxy = 0;
for (int i = 0; i < pts.Count; i++)
{
var dx = pts[i].X - mx;
var dy = pts[i].Y - my;
sxx += dx * dx;
syy += dy * dy;
sxy += dx * dy;
}
if (sxx + syy < 1e-6) return false;
thetaRad = 0.5 * Math.Atan2(2.0 * sxy, sxx - syy);
if (double.IsNaN(thetaRad) || double.IsInfinity(thetaRad)) return false;
// 主轴单位向量 v1=(cos,sin),次轴 v2=(-sin,cos)
var cos = Math.Cos(thetaRad);
var sin = Math.Sin(thetaRad);
// 投影收集,用分位数抑制离群点
var us = new double[pts.Count];
var vs = new double[pts.Count];
double maxU = double.MinValue, minU = double.MaxValue;
double maxV = double.MinValue, minV = double.MaxValue;
for (int i = 0; i < pts.Count; i++)
{
var dx = pts[i].X - mx;
var dy = pts[i].Y - my;
var u = dx * cos + dy * sin;
var v = -dx * sin + dy * cos;
us[i] = u;
vs[i] = v;
if (u > maxU) maxU = u;
if (u < minU) minU = u;
if (v > maxV) maxV = v;
if (v < minV) minV = v;
}
Array.Sort(us);
Array.Sort(vs);
int hi = (int)Math.Round((pts.Count - 1) * 0.98);
int lo = (int)Math.Round((pts.Count - 1) * 0.02);
hi = Math.Max(0, Math.Min(pts.Count - 1, hi));
lo = Math.Max(0, Math.Min(pts.Count - 1, lo));
var uHi = us[hi];
var uLo = us[lo];
var vHi = vs[hi];
var vLo = vs[lo];
var aCandidate = Math.Max(Math.Abs(uHi), Math.Abs(uLo));
var bCandidate = Math.Max(Math.Abs(vHi), Math.Abs(vLo));
if (aCandidate < 1e-3) aCandidate = Math.Max(Math.Abs(maxU), Math.Abs(minU));
if (bCandidate < 1e-3) bCandidate = Math.Max(Math.Abs(maxV), Math.Abs(minV));
a = aCandidate;
b = bCandidate;
// 保证 a 为长半轴
if (b > a)
{
var t = a; a = b; b = t;
thetaRad += Math.PI / 2;
cos = Math.Cos(thetaRad);
sin = Math.Sin(thetaRad);
}
major0 = new Point(mx - a * cos, my - a * sin);
major1 = new Point(mx + a * cos, my + a * sin);
minor0 = new Point(mx + b * sin, my - b * cos);
minor1 = new Point(mx - b * sin, my + b * cos);
return a > 1e-2 && b > 1e-2;
}
2025-08-03 16:46:33 +08:00
public StylusPointCollection GenerateFakePressureTriangle(StylusPointCollection points)
{
2025-12-27 18:22:00 +08:00
var newPoint = new StylusPointCollection();
2026-03-03 16:04:20 +08:00
2025-08-03 16:46:33 +08:00
if (Settings.InkToShape.IsInkToShapeNoFakePressureTriangle || penType == 1)
{
2025-12-27 18:22:00 +08:00
if (points.Count >= 3)
{
for (int i = 0; i < 3; i++)
{
Point start = points[i].ToPoint();
Point end = points[(i + 1) % 3].ToPoint();
var edgePoints = GeneratePointsBetween(start, end, 0.5f, 0.5f, 8.0);
if (i == 0)
{
foreach (var pt in edgePoints)
{
newPoint.Add(pt);
}
}
else
{
for (int j = 1; j < edgePoints.Count; j++)
{
newPoint.Add(edgePoints[j]);
}
}
}
Point lastPoint = points[0].ToPoint();
Point firstPoint = newPoint[0].ToPoint();
if (GetDistance(lastPoint, firstPoint) > 1.0)
{
newPoint.Add(new StylusPoint(lastPoint.X, lastPoint.Y, 0.5f));
}
}
else
{
return points;
}
2025-05-25 09:29:48 +08:00
return newPoint;
}
2025-08-03 16:46:33 +08:00
else
{
2025-12-27 18:22:00 +08:00
if (points.Count >= 3)
{
for (int i = 0; i < 3; i++)
{
Point start = points[i].ToPoint();
Point end = points[(i + 1) % 3].ToPoint();
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
Point midPoint = GetCenterPoint(start, end);
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
var startToMid = GeneratePointsBetween(start, midPoint, 0.4f, 0.8f, 8.0);
if (i == 0)
{
foreach (var pt in startToMid)
{
newPoint.Add(pt);
}
}
else
{
for (int j = 1; j < startToMid.Count; j++)
{
newPoint.Add(startToMid[j]);
}
}
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
var midToEnd = GeneratePointsBetween(midPoint, end, 0.8f, 0.4f, 8.0);
for (int j = 1; j < midToEnd.Count; j++)
{
newPoint.Add(midToEnd[j]);
}
}
Point lastPoint = points[0].ToPoint();
Point firstPoint = newPoint[0].ToPoint();
if (GetDistance(lastPoint, firstPoint) > 1.0)
{
newPoint.Add(new StylusPoint(lastPoint.X, lastPoint.Y, 0.4f));
}
}
else
{
return points;
}
2025-05-25 09:29:48 +08:00
return newPoint;
}
}
2025-12-27 18:22:00 +08:00
/// <summary>
/// 在两点之间生成多个点,用于增加图形边缘的点密度
/// </summary>
private StylusPointCollection GeneratePointsBetween(Point start, Point end, float startPressure, float endPressure, double minPointInterval = 8.0)
{
var result = new StylusPointCollection();
double distance = GetDistance(start, end);
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
if (distance < minPointInterval)
{
result.Add(new StylusPoint(start.X, start.Y, startPressure));
result.Add(new StylusPoint(end.X, end.Y, endPressure));
return result;
}
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
int pointCount = Math.Max(2, (int)(distance / minPointInterval) + 1);
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
result.Add(new StylusPoint(start.X, start.Y, startPressure));
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
for (int i = 1; i < pointCount - 1; i++)
{
double ratio = (double)i / (pointCount - 1);
double pressure = startPressure + (endPressure - startPressure) * ratio;
double x = start.X + (end.X - start.X) * ratio;
double y = start.Y + (end.Y - start.Y) * ratio;
result.Add(new StylusPoint(x, y, (float)pressure));
}
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
result.Add(new StylusPoint(end.X, end.Y, endPressure));
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
return result;
}
2025-07-28 14:40:44 +08:00
public StylusPointCollection GenerateFakePressureRectangle(StylusPointCollection points)
{
2025-12-27 18:22:00 +08:00
var newPoint = new StylusPointCollection();
2026-03-03 16:04:20 +08:00
2025-08-03 16:46:33 +08:00
if (Settings.InkToShape.IsInkToShapeNoFakePressureRectangle || penType == 1)
{
2025-12-27 18:22:00 +08:00
if (points.Count >= 4)
{
for (int i = 0; i < 4; i++)
{
Point start = points[i].ToPoint();
Point end = points[(i + 1) % 4].ToPoint();
var edgePoints = GeneratePointsBetween(start, end, 0.5f, 0.5f, 8.0);
if (i == 0)
{
foreach (var pt in edgePoints)
{
newPoint.Add(pt);
}
}
else
{
for (int j = 1; j < edgePoints.Count; j++)
{
newPoint.Add(edgePoints[j]);
}
}
}
}
else
{
return points;
}
return newPoint;
2025-05-25 09:29:48 +08:00
}
2025-07-28 14:40:44 +08:00
2025-12-27 18:22:00 +08:00
if (points.Count >= 4)
{
for (int i = 0; i < 4; i++)
{
Point start = points[i].ToPoint();
Point end = points[(i + 1) % 4].ToPoint();
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
Point midPoint = GetCenterPoint(start, end);
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
var startToMid = GeneratePointsBetween(start, midPoint, 0.4f, 0.8f, 8.0);
if (i == 0)
{
foreach (var pt in startToMid)
{
newPoint.Add(pt);
}
}
else
{
for (int j = 1; j < startToMid.Count; j++)
{
newPoint.Add(startToMid[j]);
}
}
2026-03-03 16:04:20 +08:00
2025-12-27 18:22:00 +08:00
var midToEnd = GeneratePointsBetween(midPoint, end, 0.8f, 0.4f, 8.0);
for (int j = 1; j < midToEnd.Count; j++)
{
newPoint.Add(midToEnd[j]);
}
}
}
else
{
return points;
}
2026-03-03 16:04:20 +08:00
2025-07-28 14:40:44 +08:00
return newPoint;
2025-05-25 09:29:48 +08:00
}
2025-08-03 16:46:33 +08:00
public Point GetCenterPoint(Point point1, Point point2)
{
2025-05-25 09:29:48 +08:00
return new Point((point1.X + point2.X) / 2, (point1.Y + point2.Y) / 2);
}
2025-08-03 16:46:33 +08:00
public StylusPoint GetCenterPoint(StylusPoint point1, StylusPoint point2)
{
2025-05-25 09:29:48 +08:00
return new StylusPoint((point1.X + point2.X) / 2, (point1.Y + point2.Y) / 2);
}
2025-07-24 21:02:00 +08:00
// 分辨率自适应:以1080P为基准,返回当前分辨率下的阈值倍数
private double GetResolutionScale()
{
// 以1920x1080为基准
double baseWidth = 1920.0;
double baseHeight = 1080.0;
double screenWidth = SystemParameters.PrimaryScreenWidth;
double screenHeight = SystemParameters.PrimaryScreenHeight;
// 取宽高平均缩放,防止极端比例
double scaleW = screenWidth / baseWidth;
double scaleH = screenHeight / baseHeight;
return (scaleW + scaleH) / 2.0;
}
2025-07-29 23:14:20 +08:00
#region 线
/// <summary>
/// 处理矩形参考线系统
/// </summary>
private void ProcessRectangleGuideLines(Stroke newStroke)
{
// 只有启用矩形识别时才处理
if (!Settings.InkToShape.IsInkToShapeRectangle) return;
// 检查新笔画是否为直线
if (!IsPotentialStraightLine(newStroke)) return;
Point startPoint = newStroke.StylusPoints[0].ToPoint();
Point endPoint = newStroke.StylusPoints[newStroke.StylusPoints.Count - 1].ToPoint();
// 创建新的参考线
var newGuideLine = new RectangleGuideLine(newStroke, startPoint, endPoint);
// 清理过期的参考线(超过30秒的)
CleanupExpiredGuideLines();
// 添加新参考线
rectangleGuideLines.Add(newGuideLine);
// 检查是否可以构成矩形
CheckForRectangleFormation();
}
/// <summary>
/// 清理过期的参考线
/// </summary>
private void CleanupExpiredGuideLines()
{
var expireTime = DateTime.Now.AddSeconds(-30); // 30秒过期
for (int i = rectangleGuideLines.Count - 1; i >= 0; i--)
{
var guideLine = rectangleGuideLines[i];
if (guideLine.CreatedTime < expireTime || !inkCanvas.Strokes.Contains(guideLine.OriginalStroke))
{
rectangleGuideLines.RemoveAt(i);
}
}
}
/// <summary>
/// 检查是否可以构成矩形
/// </summary>
private void CheckForRectangleFormation()
{
if (rectangleGuideLines.Count < 4) return;
// 尝试找到四条能构成矩形的直线
var rectangleLines = FindRectangleLines();
if (rectangleLines != null && rectangleLines.Count == 4)
{
// 创建矩形并替换原有直线
CreateRectangleFromLines(rectangleLines);
}
}
/// <summary>
/// 寻找能构成矩形的四条直线
/// </summary>
private List<RectangleGuideLine> FindRectangleLines()
{
// 按时间排序,优先考虑最近绘制的直线
var sortedLines = rectangleGuideLines.OrderByDescending(l => l.CreatedTime).ToList();
// 尝试不同的四条直线组合
for (int i = 0; i < sortedLines.Count - 3; i++)
{
for (int j = i + 1; j < sortedLines.Count - 2; j++)
{
for (int k = j + 1; k < sortedLines.Count - 1; k++)
{
for (int l = k + 1; l < sortedLines.Count; l++)
{
var lines = new List<RectangleGuideLine> { sortedLines[i], sortedLines[j], sortedLines[k], sortedLines[l] };
if (CanFormRectangle(lines))
{
return lines;
}
}
}
}
}
return null;
}
/// <summary>
/// 判断四条直线是否能构成矩形
/// </summary>
private bool CanFormRectangle(List<RectangleGuideLine> lines)
{
if (lines.Count != 4) return false;
// 分类水平线和垂直线
var horizontalLines = lines.Where(l => l.IsHorizontal).ToList();
var verticalLines = lines.Where(l => l.IsVertical).ToList();
// 必须有2条水平线和2条垂直线
if (horizontalLines.Count != 2 || verticalLines.Count != 2) return false;
// 检查端点相交关系
return CheckEndpointConnections(horizontalLines, verticalLines);
}
/// <summary>
/// 检查端点相交关系
/// </summary>
private bool CheckEndpointConnections(List<RectangleGuideLine> horizontalLines, List<RectangleGuideLine> verticalLines)
{
// 收集所有端点
var allEndpoints = new List<Point>();
foreach (var line in horizontalLines.Concat(verticalLines))
{
allEndpoints.Add(line.StartPoint);
allEndpoints.Add(line.EndPoint);
}
// 检查是否有4个相交点(允许一定误差)
var intersectionPoints = new List<Point>();
foreach (var hLine in horizontalLines)
{
foreach (var vLine in verticalLines)
{
var intersection = GetLineIntersection(hLine, vLine);
if (intersection.HasValue)
{
// 检查交点是否在两条线段的端点附近
if (IsPointNearLineEndpoints(intersection.Value, hLine) &&
IsPointNearLineEndpoints(intersection.Value, vLine))
{
intersectionPoints.Add(intersection.Value);
}
}
}
}
// 需要有4个交点才能构成矩形
return intersectionPoints.Count >= 4;
}
/// <summary>
/// 计算两条直线的交点
/// </summary>
private Point? GetLineIntersection(RectangleGuideLine line1, RectangleGuideLine line2)
{
double x1 = line1.StartPoint.X, y1 = line1.StartPoint.Y;
double x2 = line1.EndPoint.X, y2 = line1.EndPoint.Y;
double x3 = line2.StartPoint.X, y3 = line2.StartPoint.Y;
double x4 = line2.EndPoint.X, y4 = line2.EndPoint.Y;
double denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (Math.Abs(denom) < 1e-10) return null; // 平行线
double t = ((x1 - x3) * (y3 - y4) - (y1 - y3) * (x3 - x4)) / denom;
double u = -((x1 - x2) * (y1 - y3) - (y1 - y2) * (x1 - x3)) / denom;
double intersectionX = x1 + t * (x2 - x1);
double intersectionY = y1 + t * (y2 - y1);
return new Point(intersectionX, intersectionY);
}
/// <summary>
/// 检查点是否在直线端点附近
/// </summary>
private bool IsPointNearLineEndpoints(Point point, RectangleGuideLine line)
{
double distToStart = GetDistance(point, line.StartPoint);
double distToEnd = GetDistance(point, line.EndPoint);
return distToStart <= RECTANGLE_ENDPOINT_THRESHOLD || distToEnd <= RECTANGLE_ENDPOINT_THRESHOLD;
}
/// <summary>
/// 从四条直线创建矩形
/// </summary>
private void CreateRectangleFromLines(List<RectangleGuideLine> lines)
{
try
{
// 计算矩形的四个角点
var corners = CalculateRectangleCorners(lines);
if (corners == null || corners.Count != 4) return;
// 创建矩形笔画
var pointList = new List<Point>(corners) { corners[0] }; // 闭合矩形
var point = new StylusPointCollection(pointList);
var rectangleStroke = new Stroke(GenerateFakePressureRectangle(point))
{
DrawingAttributes = inkCanvas.DefaultDrawingAttributes.Clone()
};
rectangleStroke.AddPropertyData(Helpers.ModernInkAnalyzer.ShapeStrokePropertyGuid, true);
2025-07-29 23:14:20 +08:00
// 移除原有的四条直线
SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition;
foreach (var line in lines)
{
if (inkCanvas.Strokes.Contains(line.OriginalStroke))
{
inkCanvas.Strokes.Remove(line.OriginalStroke);
}
}
// 添加新的矩形
inkCanvas.Strokes.Add(rectangleStroke);
_currentCommitType = CommitReason.UserInput;
// 清理参考线
foreach (var line in lines)
{
rectangleGuideLines.Remove(line);
}
// 清空新笔画集合,避免重复处理
newStrokes = new StrokeCollection();
Debug.WriteLine("成功创建矩形参考线矩形");
}
catch (Exception ex)
{
Debug.WriteLine($"创建矩形时出错: {ex.Message}");
}
}
/// <summary>
/// 计算矩形的四个角点
/// </summary>
private List<Point> CalculateRectangleCorners(List<RectangleGuideLine> lines)
{
var horizontalLines = lines.Where(l => l.IsHorizontal).ToList();
var verticalLines = lines.Where(l => l.IsVertical).ToList();
if (horizontalLines.Count != 2 || verticalLines.Count != 2) return null;
var corners = new List<Point>();
// 计算四个交点
foreach (var hLine in horizontalLines)
{
foreach (var vLine in verticalLines)
{
var intersection = GetLineIntersection(hLine, vLine);
if (intersection.HasValue)
{
corners.Add(intersection.Value);
}
}
}
if (corners.Count != 4) return null;
// 按顺序排列角点(顺时针或逆时针)
return SortRectangleCorners(corners);
}
/// <summary>
/// 按顺序排列矩形角点
/// </summary>
private List<Point> SortRectangleCorners(List<Point> corners)
{
if (corners.Count != 4) return corners;
// 计算中心点
double centerX = corners.Average(p => p.X);
double centerY = corners.Average(p => p.Y);
var center = new Point(centerX, centerY);
// 按角度排序
return corners.OrderBy(p => Math.Atan2(p.Y - center.Y, p.X - center.X)).ToList();
}
2026-04-05 12:17:02 +08:00
/// <summary>
/// 异步墨迹平滑将画布上的 <paramref name="fromStroke"/> 替换为 <paramref name="toStroke"/> 后,把手写字形替换批次里的画布引用一并迁移,使识别仍命中「原始点快照」字典项。
/// </summary>
private void MigrateHandwritingBeautifyCanvasStrokeReference(Stroke fromStroke, Stroke toStroke)
{
if (fromStroke == null || toStroke == null || ReferenceEquals(fromStroke, toStroke))
return;
if (!Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify)
return;
if (_handwritingBeautifyInkInputByCanvasStroke.TryGetValue(fromStroke, out var inkInput))
{
_handwritingBeautifyInkInputByCanvasStroke.Remove(fromStroke);
_handwritingBeautifyInkInputByCanvasStroke[toStroke] = inkInput;
}
for (var i = 0; i < _handwritingRecentStrokesForBeautify.Count; i++)
{
if (ReferenceEquals(_handwritingRecentStrokesForBeautify[i], fromStroke))
_handwritingRecentStrokesForBeautify[i] = toStroke;
}
}
2026-03-29 12:24:13 +08:00
/// <summary>
/// 收笔后:在墨迹转形状(若启用)完成之后,将笔画并入批次并启动/重置停笔防抖计时器,再于延迟后多笔合并矫正。
/// </summary>
private void PruneHandwritingBeautifyBatch()
{
for (var i = _handwritingRecentStrokesForBeautify.Count - 1; i >= 0; i--)
{
2026-04-04 22:16:37 +08:00
var s = _handwritingRecentStrokesForBeautify[i];
if (!inkCanvas.Strokes.Contains(s))
{
_handwritingBeautifyInkInputByCanvasStroke.Remove(s);
2026-03-29 12:24:13 +08:00
_handwritingRecentStrokesForBeautify.RemoveAt(i);
2026-04-04 22:16:37 +08:00
}
2026-03-29 12:24:13 +08:00
}
}
private void EnsureHandwritingBeautifyDebounceTimer()
{
if (_handwritingBeautifyDebounceTimer != null)
return;
_handwritingBeautifyDebounceTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher)
{
Interval = TimeSpan.FromMilliseconds(HandwritingBeautifyDebounceMs)
};
_handwritingBeautifyDebounceTimer.Tick += HandwritingBeautifyDebounceTimer_Tick;
}
2026-04-04 22:16:37 +08:00
/// <summary>深拷贝点集,供手写纠正识别输入(与笔锋/二次压感合成前的画布数据一致)。</summary>
private static StylusPointCollection CloneStylusPointCollectionForHandwritingInput(StylusPointCollection source)
{
if (source == null || source.Count == 0)
return null;
var copy = new StylusPointCollection();
foreach (StylusPoint p in source)
copy.Add(new StylusPoint(p.X, p.Y, p.PressureFactor));
return copy;
}
2026-03-29 12:24:13 +08:00
/// <summary>并入批次并重置 1s 计时器(多笔需停笔满延迟后才矫正)。</summary>
2026-04-04 22:16:37 +08:00
/// <param name="preBrushHandwritingPoints">笔锋与后续 InkStyle 压感合成前的点集;为 null 时识别输入与画布笔画一致(兼容旧行为)。</param>
private void ScheduleHandwritingGlyphReplaceAfterStrokeCollected(
Stroke strokeForBeautify,
bool isBoardBrushStroke,
StylusPointCollection preBrushHandwritingPoints = null)
2026-03-29 12:24:13 +08:00
{
if (!Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify)
return;
if (isBoardBrushStroke)
return;
if (strokeForBeautify == null || strokeForBeautify.DrawingAttributes?.IsHighlighter == true)
return;
if (!inkCanvas.Strokes.Contains(strokeForBeautify))
return;
_handwritingBeautifyScheduleRevision++;
2026-04-04 22:16:37 +08:00
if (preBrushHandwritingPoints != null && preBrushHandwritingPoints.Count > 0)
{
2026-04-05 12:17:02 +08:00
// 再拷贝一份给识别专用 Stroke,避免与外部 StylusPointCollection 或 WPF Stroke 内部共享后被改写。
var ptsForRecognizer = CloneStylusPointCollectionForHandwritingInput(preBrushHandwritingPoints);
if (ptsForRecognizer != null && ptsForRecognizer.Count > 0)
2026-04-04 22:16:37 +08:00
{
2026-04-05 12:17:02 +08:00
_handwritingBeautifyInkInputByCanvasStroke[strokeForBeautify] = new Stroke(ptsForRecognizer)
{
DrawingAttributes = strokeForBeautify.DrawingAttributes.Clone()
};
}
else
{
_handwritingBeautifyInkInputByCanvasStroke.Remove(strokeForBeautify);
}
2026-04-04 22:16:37 +08:00
}
else
{
_handwritingBeautifyInkInputByCanvasStroke.Remove(strokeForBeautify);
}
2026-03-29 12:24:13 +08:00
var alreadyInBatch = false;
foreach (Stroke x in _handwritingRecentStrokesForBeautify)
{
if (ReferenceEquals(x, strokeForBeautify))
{
alreadyInBatch = true;
break;
}
}
if (!alreadyInBatch)
_handwritingRecentStrokesForBeautify.Add(strokeForBeautify);
PruneHandwritingBeautifyBatch();
while (_handwritingRecentStrokesForBeautify.Count > HandwritingBeautifyMaxRecentStrokes)
2026-04-05 18:52:19 +08:00
{
var evicted = _handwritingRecentStrokesForBeautify[0];
2026-03-29 12:24:13 +08:00
_handwritingRecentStrokesForBeautify.RemoveAt(0);
2026-04-05 18:52:19 +08:00
_handwritingBeautifyInkInputByCanvasStroke.Remove(evicted);
}
2026-03-29 12:24:13 +08:00
EnsureHandwritingBeautifyDebounceTimer();
_handwritingBeautifyDebounceTimer.Stop();
_handwritingBeautifyDebounceTimer.Interval = TimeSpan.FromMilliseconds(HandwritingBeautifyDebounceMs);
_handwritingBeautifyDebounceTimer.Start();
}
private async void HandwritingBeautifyDebounceTimer_Tick(object sender, EventArgs e)
{
if (_handwritingBeautifyDebounceTimer != null)
_handwritingBeautifyDebounceTimer.Stop();
var revisionWhenIdle = _handwritingBeautifyScheduleRevision;
try
{
await HandwritingGlyphReplaceFromBatchCoreAsync(revisionWhenIdle).ConfigureAwait(true);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("[手写体] 停笔防抖矫正异常: " + ex.Message, LogHelper.LogType.Warning);
Debug.WriteLine(ex);
}
}
private async Task HandwritingGlyphReplaceFromBatchCoreAsync(ulong revisionWhenIdle)
{
await InkToShapeSerial.WaitAsync().ConfigureAwait(true);
try
{
if (_handwritingBeautifyScheduleRevision != revisionWhenIdle)
return;
PruneHandwritingBeautifyBatch();
2026-04-04 22:16:37 +08:00
var canvasStrokes = new StrokeCollection();
var recognitionInput = new StrokeCollection();
2026-03-29 12:24:13 +08:00
foreach (Stroke s in _handwritingRecentStrokesForBeautify)
{
2026-04-04 22:16:37 +08:00
if (!inkCanvas.Strokes.Contains(s))
continue;
canvasStrokes.Add(s);
if (_handwritingBeautifyInkInputByCanvasStroke.TryGetValue(s, out var inkInput) && inkInput != null)
2026-04-05 12:17:02 +08:00
{
2026-04-04 22:16:37 +08:00
recognitionInput.Add(inkInput);
2026-04-05 12:17:02 +08:00
}
2026-04-04 22:16:37 +08:00
else
2026-04-05 12:17:02 +08:00
{
LogHelper.WriteLogToFile(
"[手写体] 批次识别输入回退为画布笔画(未命中原始点快照)。画布点数=" +
(s.StylusPoints?.Count ?? 0),
LogHelper.LogType.Info);
2026-04-04 22:16:37 +08:00
recognitionInput.Add(s);
2026-04-05 12:17:02 +08:00
}
2026-03-29 12:24:13 +08:00
}
2026-04-04 22:16:37 +08:00
if (canvasStrokes.Count == 0)
2026-03-29 12:24:13 +08:00
return;
if (_handwritingBeautifyScheduleRevision != revisionWhenIdle)
return;
var shapeMode = ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine);
2026-04-04 22:16:37 +08:00
var result = await InkRecognizeHelper.CorrectHandwritingStrokesUnifiedAsync(recognitionInput, shapeMode).ConfigureAwait(true);
2026-03-29 12:24:13 +08:00
if (_handwritingBeautifyScheduleRevision != revisionWhenIdle)
return;
if (result == null || result.Count == 0)
return;
2026-04-04 22:16:37 +08:00
if (ReferenceEquals(result, recognitionInput))
2026-03-29 12:24:13 +08:00
return;
var anyInputStillPresent = false;
2026-04-04 22:16:37 +08:00
foreach (Stroke s in canvasStrokes)
2026-03-29 12:24:13 +08:00
{
if (inkCanvas.Strokes.Contains(s))
{
anyInputStillPresent = true;
break;
}
}
if (!anyInputStillPresent)
return;
SetNewBackupOfStroke();
_currentCommitType = CommitReason.ShapeRecognition;
2026-04-04 22:16:37 +08:00
foreach (Stroke s in canvasStrokes)
2026-03-29 12:24:13 +08:00
{
if (inkCanvas.Strokes.Contains(s))
inkCanvas.Strokes.Remove(s);
}
foreach (Stroke s in result)
inkCanvas.Strokes.Add(s);
_currentCommitType = CommitReason.UserInput;
2026-04-04 22:16:37 +08:00
foreach (Stroke s in canvasStrokes)
{
2026-03-29 12:24:13 +08:00
_handwritingRecentStrokesForBeautify.Remove(s);
2026-04-04 22:16:37 +08:00
_handwritingBeautifyInkInputByCanvasStroke.Remove(s);
}
2026-03-29 12:24:13 +08:00
PruneHandwritingBeautifyBatch();
}
catch (Exception ex)
{
LogHelper.WriteLogToFile("[手写体] 收笔替换异常: " + ex.Message, LogHelper.LogType.Warning);
}
finally
{
InkToShapeSerial.Release();
}
}
2025-07-29 23:14:20 +08:00
#endregion
2025-05-25 09:29:48 +08:00
}
}