diff --git a/Directory.Packages.props b/Directory.Packages.props
index f43665b..2678544 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -30,7 +30,7 @@
-
+
diff --git a/LanMountainDesktop.Tests/WhiteboardNotePersistenceServiceTests.cs b/LanMountainDesktop.Tests/WhiteboardNotePersistenceServiceTests.cs
index 3274591..8c0c939 100644
--- a/LanMountainDesktop.Tests/WhiteboardNotePersistenceServiceTests.cs
+++ b/LanMountainDesktop.Tests/WhiteboardNotePersistenceServiceTests.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Text.Json;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using Xunit;
@@ -9,24 +10,102 @@ namespace LanMountainDesktop.Tests;
public sealed class WhiteboardNotePersistenceServiceTests
{
[Fact]
- public void SaveNote_ThenLoadNote_RoundTripsSnapshot()
+ public void SaveNote_ThenLoadNote_RoundTripsFileSnapshot()
{
using var sandbox = new WhiteboardNotePersistenceSandbox();
var service = sandbox.CreateService();
var snapshot = CreateSampleSnapshot();
- service.SaveNote("DesktopWhiteboard", "whiteboard-1", snapshot, retentionDays: 15);
-
+ var saved = service.SaveNote("DesktopWhiteboard", "whiteboard-1", snapshot, retentionDays: 15);
var loaded = service.LoadNote("DesktopWhiteboard", "whiteboard-1", retentionDays: 15);
+ Assert.True(saved);
+ Assert.True(File.Exists(sandbox.GetNoteFilePath("DesktopWhiteboard", "whiteboard-1")));
+ Assert.Equal(2, loaded.Version);
Assert.Single(loaded.Strokes);
Assert.Equal(2, loaded.Strokes[0].Points.Count);
+ Assert.Equal("M 0 0 L 12 12", loaded.Strokes[0].PathSvgData);
Assert.Equal("#FF112233", loaded.Strokes[0].Color);
+ Assert.Equal(1.75d, loaded.ViewportZoom);
+ Assert.Equal(-24d, loaded.ViewportOffsetX);
+ Assert.Equal(-36d, loaded.ViewportOffsetY);
Assert.True(loaded.SavedUtc > DateTimeOffset.MinValue);
+ Assert.True(loaded.ExpiresUtc > loaded.SavedUtc);
}
[Fact]
- public void LoadNote_RemovesExpiredSnapshot_WhenRetentionExceeded()
+ public void SaveNote_WithReadOnlyExistingFile_ReturnsFalseAndKeepsOldFile()
+ {
+ using var sandbox = new WhiteboardNotePersistenceSandbox();
+ var service = sandbox.CreateService();
+ var notePath = sandbox.GetNoteFilePath("DesktopWhiteboard", "read-only-board");
+
+ Assert.True(service.SaveNote("DesktopWhiteboard", "read-only-board", CreateSampleSnapshot("#FF112233"), retentionDays: 15));
+ File.SetAttributes(notePath, File.GetAttributes(notePath) | FileAttributes.ReadOnly);
+
+ try
+ {
+ var saved = service.SaveNote("DesktopWhiteboard", "read-only-board", CreateSampleSnapshot("#FF445566"), retentionDays: 15);
+ var loaded = service.LoadNote("DesktopWhiteboard", "read-only-board", retentionDays: 15);
+
+ Assert.False(saved);
+ Assert.Equal("#FF112233", loaded.Strokes[0].Color);
+ }
+ finally
+ {
+ File.SetAttributes(notePath, FileAttributes.Normal);
+ }
+ }
+
+ [Fact]
+ public void SaveNote_WithEmptySnapshot_OverwritesOldContent()
+ {
+ using var sandbox = new WhiteboardNotePersistenceSandbox();
+ var service = sandbox.CreateService();
+
+ Assert.True(service.SaveNote("DesktopWhiteboard", "clear-board", CreateSampleSnapshot(), retentionDays: 15));
+ Assert.True(service.SaveNote("DesktopWhiteboard", "clear-board", new WhiteboardNoteSnapshot
+ {
+ CanvasWidth = 320,
+ CanvasHeight = 180,
+ ViewportZoom = 2d,
+ ViewportOffsetX = -40d,
+ ViewportOffsetY = -20d
+ }, retentionDays: 15));
+
+ var loaded = service.LoadNote("DesktopWhiteboard", "clear-board", retentionDays: 15);
+
+ Assert.Empty(loaded.Strokes);
+ Assert.Equal(2d, loaded.ViewportZoom);
+ Assert.Equal(-40d, loaded.ViewportOffsetX);
+ Assert.Equal(-20d, loaded.ViewportOffsetY);
+ Assert.True(File.Exists(sandbox.GetNoteFilePath("DesktopWhiteboard", "clear-board")));
+ }
+
+ [Fact]
+ public void LoadNote_WithOldJsonWithoutViewport_UsesDefaultViewport()
+ {
+ using var sandbox = new WhiteboardNotePersistenceSandbox();
+ var service = sandbox.CreateService();
+ sandbox.WriteRawNoteJson("DesktopWhiteboard", "old-json-board", """
+ {
+ "version": 2,
+ "canvasWidth": 320,
+ "canvasHeight": 180,
+ "backgroundColor": "#FFFFFFFF",
+ "strokes": []
+ }
+ """);
+
+ var loaded = service.LoadNote("DesktopWhiteboard", "old-json-board", retentionDays: 15);
+
+ Assert.Equal(1d, loaded.ViewportZoom);
+ Assert.Equal(0d, loaded.ViewportOffsetX);
+ Assert.Equal(0d, loaded.ViewportOffsetY);
+ }
+
+ [Fact]
+ public void LoadNote_RemovesExpiredFile_WhenRetentionExceeded()
{
using var sandbox = new WhiteboardNotePersistenceSandbox();
var service = sandbox.CreateService();
@@ -37,11 +116,11 @@ public sealed class WhiteboardNotePersistenceServiceTests
var loaded = service.LoadNote("DesktopWhiteboard", "expired-board", retentionDays: 7);
Assert.Empty(loaded.Strokes);
- Assert.False(sandbox.Exists("DesktopWhiteboard", "expired-board"));
+ Assert.False(File.Exists(sandbox.GetNoteFilePath("DesktopWhiteboard", "expired-board")));
}
[Fact]
- public void DeleteExpiredNotesBatch_RemovesExpiredRows_AndKeepsFreshRows()
+ public void DeleteExpiredNotesBatch_RemovesExpiredFiles_AndKeepsFreshFiles()
{
using var sandbox = new WhiteboardNotePersistenceSandbox();
var service = sandbox.CreateService();
@@ -57,22 +136,59 @@ public sealed class WhiteboardNotePersistenceServiceTests
var deletedCount = service.DeleteExpiredNotesBatch(batchSize: 10);
Assert.Equal(2, deletedCount);
- Assert.False(sandbox.Exists("DesktopWhiteboard", "expired-a"));
- Assert.False(sandbox.Exists("DesktopWhiteboard", "expired-b"));
- Assert.True(sandbox.Exists("DesktopWhiteboard", "fresh-c"));
+ Assert.False(File.Exists(sandbox.GetNoteFilePath("DesktopWhiteboard", "expired-a")));
+ Assert.False(File.Exists(sandbox.GetNoteFilePath("DesktopWhiteboard", "expired-b")));
+ Assert.True(File.Exists(sandbox.GetNoteFilePath("DesktopWhiteboard", "fresh-c")));
}
- private static WhiteboardNoteSnapshot CreateSampleSnapshot()
+ [Fact]
+ public void LoadNote_MigratesLegacyDatabaseSnapshot_WhenFileMissing()
+ {
+ using var sandbox = new WhiteboardNotePersistenceSandbox();
+ sandbox.SaveLegacyNote("DesktopWhiteboard", "legacy-board", CreateSampleSnapshot("#FF778899"), retentionDays: 15);
+ var service = sandbox.CreateService();
+
+ var loaded = service.LoadNote("DesktopWhiteboard", "legacy-board", retentionDays: 15);
+
+ Assert.Single(loaded.Strokes);
+ Assert.Equal("#FF778899", loaded.Strokes[0].Color);
+ Assert.True(File.Exists(sandbox.GetNoteFilePath("DesktopWhiteboard", "legacy-board")));
+ Assert.False(sandbox.LegacyExists("DesktopWhiteboard", "legacy-board"));
+ }
+
+ [Fact]
+ public void DeleteNote_RemovesFileAndLegacyRow()
+ {
+ using var sandbox = new WhiteboardNotePersistenceSandbox();
+ sandbox.SaveLegacyNote("DesktopWhiteboard", "delete-board", CreateSampleSnapshot(), retentionDays: 15);
+ var service = sandbox.CreateService();
+ service.SaveNote("DesktopWhiteboard", "delete-board", CreateSampleSnapshot(), retentionDays: 15);
+
+ var deleted = service.DeleteNote("DesktopWhiteboard", "delete-board");
+
+ Assert.True(deleted);
+ Assert.False(File.Exists(sandbox.GetNoteFilePath("DesktopWhiteboard", "delete-board")));
+ Assert.False(sandbox.LegacyExists("DesktopWhiteboard", "delete-board"));
+ }
+
+ private static WhiteboardNoteSnapshot CreateSampleSnapshot(string color = "#FF112233")
{
return new WhiteboardNoteSnapshot
{
+ CanvasWidth = 320,
+ CanvasHeight = 180,
+ BackgroundColor = "#FFFFFFFF",
+ ViewportZoom = 1.75d,
+ ViewportOffsetX = -24d,
+ ViewportOffsetY = -36d,
Strokes =
[
new WhiteboardStrokeSnapshot
{
- Color = "#FF112233",
+ Color = color,
InkThickness = 3.5d,
IgnorePressure = true,
+ PathSvgData = "M 0 0 L 12 12",
Points =
[
new WhiteboardStylusPointSnapshot { X = 12, Y = 34, Pressure = 0.4d, Width = 2, Height = 2 },
@@ -91,40 +207,93 @@ public sealed class WhiteboardNotePersistenceServiceTests
Guid.NewGuid().ToString("N"));
private readonly string _databasePath;
+ private readonly string _whiteboardsRootDirectory;
public WhiteboardNotePersistenceSandbox()
{
Directory.CreateDirectory(_directoryPath);
_databasePath = Path.Combine(_directoryPath, "whiteboard-tests.db");
+ _whiteboardsRootDirectory = Path.Combine(_directoryPath, "Whiteboards");
}
public WhiteboardNotePersistenceService CreateService()
{
- return new WhiteboardNotePersistenceService(new AppDatabaseService(_databasePath));
+ return new WhiteboardNotePersistenceService(
+ _whiteboardsRootDirectory,
+ new AppDatabaseService(_databasePath));
+ }
+
+ public string GetNoteFilePath(string componentId, string placementId)
+ {
+ return CreateService().GetNoteFilePathForTests(componentId, placementId);
}
public void OverrideSavedTimestamp(string componentId, string placementId, DateTimeOffset savedUtc, int retentionDays)
{
- var expiresUtc = savedUtc.AddDays(WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays));
+ var notePath = GetNoteFilePath(componentId, placementId);
+ var snapshot = JsonSerializer.Deserialize(
+ File.ReadAllText(notePath),
+ new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new WhiteboardNoteSnapshot();
+ snapshot.SavedUtc = savedUtc;
+ snapshot.ExpiresUtc = savedUtc.AddDays(WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays));
+ File.WriteAllText(notePath, JsonSerializer.Serialize(snapshot, new JsonSerializerOptions { WriteIndented = true }));
+ }
+
+ public void SaveLegacyNote(string componentId, string placementId, WhiteboardNoteSnapshot snapshot, int retentionDays)
+ {
+ var nowUtc = DateTimeOffset.UtcNow;
+ var expiresUtc = nowUtc.AddDays(WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays));
using var connection = new AppDatabaseService(_databasePath).OpenConnection();
+ using (var schemaCommand = connection.CreateCommand())
+ {
+ schemaCommand.CommandText = """
+ CREATE TABLE IF NOT EXISTS whiteboard_notes (
+ component_id TEXT NOT NULL,
+ placement_id TEXT NOT NULL,
+ note_json TEXT NOT NULL,
+ saved_at_utc_ms INTEGER NOT NULL,
+ expires_at_utc_ms INTEGER NOT NULL,
+ updated_at_utc_ms INTEGER NOT NULL,
+ PRIMARY KEY (component_id, placement_id)
+ );
+ """;
+ schemaCommand.ExecuteNonQuery();
+ }
+
using var command = connection.CreateCommand();
command.CommandText = """
- UPDATE whiteboard_notes
- SET saved_at_utc_ms = $savedAtUtcMs,
- expires_at_utc_ms = $expiresAtUtcMs,
- updated_at_utc_ms = $updatedAtUtcMs
- WHERE component_id = $componentId
- AND placement_id = $placementId;
+ INSERT INTO whiteboard_notes(
+ component_id,
+ placement_id,
+ note_json,
+ saved_at_utc_ms,
+ expires_at_utc_ms,
+ updated_at_utc_ms)
+ VALUES(
+ $componentId,
+ $placementId,
+ $noteJson,
+ $savedAtUtcMs,
+ $expiresAtUtcMs,
+ $updatedAtUtcMs);
""";
- command.Parameters.AddWithValue("$savedAtUtcMs", savedUtc.ToUnixTimeMilliseconds());
- command.Parameters.AddWithValue("$expiresAtUtcMs", expiresUtc.ToUnixTimeMilliseconds());
- command.Parameters.AddWithValue("$updatedAtUtcMs", DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
command.Parameters.AddWithValue("$componentId", componentId);
command.Parameters.AddWithValue("$placementId", placementId);
+ command.Parameters.AddWithValue("$noteJson", JsonSerializer.Serialize(snapshot));
+ command.Parameters.AddWithValue("$savedAtUtcMs", nowUtc.ToUnixTimeMilliseconds());
+ command.Parameters.AddWithValue("$expiresAtUtcMs", expiresUtc.ToUnixTimeMilliseconds());
+ command.Parameters.AddWithValue("$updatedAtUtcMs", nowUtc.ToUnixTimeMilliseconds());
command.ExecuteNonQuery();
}
- public bool Exists(string componentId, string placementId)
+ public void WriteRawNoteJson(string componentId, string placementId, string json)
+ {
+ var notePath = GetNoteFilePath(componentId, placementId);
+ Directory.CreateDirectory(Path.GetDirectoryName(notePath)!);
+ File.WriteAllText(notePath, json);
+ }
+
+ public bool LegacyExists(string componentId, string placementId)
{
using var connection = new AppDatabaseService(_databasePath).OpenConnection();
using var command = connection.CreateCommand();
diff --git a/LanMountainDesktop.Tests/WhiteboardStrokePathBuilderTests.cs b/LanMountainDesktop.Tests/WhiteboardStrokePathBuilderTests.cs
new file mode 100644
index 0000000..b478c89
--- /dev/null
+++ b/LanMountainDesktop.Tests/WhiteboardStrokePathBuilderTests.cs
@@ -0,0 +1,77 @@
+using DotNetCampus.Inking.Primitive;
+using LanMountainDesktop.Views.Components;
+using Xunit;
+
+namespace LanMountainDesktop.Tests;
+
+public sealed class WhiteboardStrokePathBuilderTests
+{
+ [Fact]
+ public void BuildPath_WithEmptyPointList_ReturnsEmptyPath()
+ {
+ using var path = WhiteboardStrokePathBuilder.BuildPath(Array.Empty(), inkThickness: 3d);
+
+ Assert.True(path.IsEmpty);
+ }
+
+ [Fact]
+ public void BuildPath_WithSinglePoint_CreatesVisibleStroke()
+ {
+ using var path = WhiteboardStrokePathBuilder.BuildPath(
+ [CreatePoint(24, 32)],
+ inkThickness: 6d);
+
+ Assert.False(path.IsEmpty);
+ Assert.True(path.Bounds.Width >= 5.5f);
+ Assert.True(path.Bounds.Height >= 5.5f);
+ }
+
+ [Fact]
+ public void BuildPath_WithMultiplePoints_CreatesFilledStroke()
+ {
+ using var path = WhiteboardStrokePathBuilder.BuildPath(
+ [
+ CreatePoint(10, 10),
+ CreatePoint(30, 18),
+ CreatePoint(52, 14)
+ ],
+ inkThickness: 4d);
+
+ Assert.False(path.IsEmpty);
+ Assert.True(path.Bounds.Width > 40f);
+ Assert.True(path.Bounds.Height > 4f);
+ }
+
+ [Fact]
+ public void BuildPath_WithThickerStroke_ExpandsStrokeBounds()
+ {
+ var points = new[]
+ {
+ CreatePoint(10, 10),
+ CreatePoint(80, 10)
+ };
+
+ using var thinPath = WhiteboardStrokePathBuilder.BuildPath(points, inkThickness: 1d);
+ using var thickPath = WhiteboardStrokePathBuilder.BuildPath(points, inkThickness: 8d);
+
+ Assert.True(thickPath.Bounds.Height > thinPath.Bounds.Height);
+ }
+
+ [Fact]
+ public void BuildPath_WithNonFinitePoints_UsesRemainingFinitePoints()
+ {
+ using var path = WhiteboardStrokePathBuilder.BuildPath(
+ [
+ CreatePoint(double.NaN, 10),
+ CreatePoint(20, 20)
+ ],
+ inkThickness: 4d);
+
+ Assert.False(path.IsEmpty);
+ }
+
+ private static InkStylusPoint CreatePoint(double x, double y)
+ {
+ return new InkStylusPoint(x, y, pressure: 1f);
+ }
+}
diff --git a/LanMountainDesktop.Tests/WhiteboardSvgImportServiceTests.cs b/LanMountainDesktop.Tests/WhiteboardSvgImportServiceTests.cs
new file mode 100644
index 0000000..1b32724
--- /dev/null
+++ b/LanMountainDesktop.Tests/WhiteboardSvgImportServiceTests.cs
@@ -0,0 +1,65 @@
+using System.IO;
+using System.Text;
+using LanMountainDesktop.Services;
+using Xunit;
+
+namespace LanMountainDesktop.Tests;
+
+public sealed class WhiteboardSvgImportServiceTests
+{
+ [Fact]
+ public void Import_WithFilledPath_CreatesStaticStrokeSnapshot()
+ {
+ using var stream = ToStream("""
+
+ """);
+
+ var result = WhiteboardSvgImportService.Import(stream, targetWidth: 200, targetHeight: 100);
+
+ Assert.Single(result.Strokes);
+ Assert.Equal("#FF112233", result.Strokes[0].Color);
+ Assert.Empty(result.Strokes[0].Points);
+ Assert.False(string.IsNullOrWhiteSpace(result.Strokes[0].PathSvgData));
+ }
+
+ [Fact]
+ public void Import_WithStrokePath_ConvertsStrokeToFilledPath()
+ {
+ using var stream = ToStream("""
+
+ """);
+
+ var result = WhiteboardSvgImportService.Import(stream, targetWidth: 100, targetHeight: 100);
+
+ Assert.Single(result.Strokes);
+ Assert.Equal("#FFFF0000", result.Strokes[0].Color);
+ Assert.True(result.Strokes[0].InkThickness >= 6d);
+ Assert.Empty(result.Strokes[0].Points);
+ Assert.False(string.IsNullOrWhiteSpace(result.Strokes[0].PathSvgData));
+ }
+
+ [Fact]
+ public void Import_WithStylePresentationAttributes_ParsesStyleValues()
+ {
+ using var stream = ToStream("""
+
+ """);
+
+ var result = WhiteboardSvgImportService.Import(stream, targetWidth: 100, targetHeight: 100);
+
+ Assert.Single(result.Strokes);
+ Assert.Equal("#FF00FF00", result.Strokes[0].Color);
+ Assert.True(result.Strokes[0].InkThickness >= 4d);
+ }
+
+ private static MemoryStream ToStream(string svg)
+ {
+ return new MemoryStream(Encoding.UTF8.GetBytes(svg));
+ }
+}
diff --git a/LanMountainDesktop.Tests/WhiteboardViewportHelperTests.cs b/LanMountainDesktop.Tests/WhiteboardViewportHelperTests.cs
new file mode 100644
index 0000000..2488bb9
--- /dev/null
+++ b/LanMountainDesktop.Tests/WhiteboardViewportHelperTests.cs
@@ -0,0 +1,68 @@
+using Avalonia;
+using LanMountainDesktop.Views.Components;
+using Xunit;
+
+namespace LanMountainDesktop.Tests;
+
+public sealed class WhiteboardViewportHelperTests
+{
+ [Fact]
+ public void ZoomAt_WithCenterAnchor_KeepsAnchorLogicalPointStable()
+ {
+ var viewportSize = new Size(200, 100);
+ var canvasSize = new Size(400, 200);
+ var state = new WhiteboardViewportState(1d, default);
+ var anchor = new Point(100, 50);
+ var before = WhiteboardViewportHelper.ToLogicalPoint(state, anchor);
+
+ var zoomed = WhiteboardViewportHelper.ZoomAt(state, 2d, anchor, viewportSize, canvasSize);
+ var after = WhiteboardViewportHelper.ToLogicalPoint(zoomed, anchor);
+
+ Assert.Equal(before.X, after.X, precision: 3);
+ Assert.Equal(before.Y, after.Y, precision: 3);
+ }
+
+ [Fact]
+ public void PanBy_ClampsToScaledCanvasBounds()
+ {
+ var viewportSize = new Size(100, 100);
+ var canvasSize = new Size(200, 200);
+ var state = new WhiteboardViewportState(2d, default);
+
+ var positive = WhiteboardViewportHelper.PanBy(state, new Vector(500, 500), viewportSize, canvasSize);
+ var negative = WhiteboardViewportHelper.PanBy(state, new Vector(-500, -500), viewportSize, canvasSize);
+
+ Assert.Equal(0d, positive.Offset.X, precision: 3);
+ Assert.Equal(0d, positive.Offset.Y, precision: 3);
+ Assert.Equal(-300d, negative.Offset.X, precision: 3);
+ Assert.Equal(-300d, negative.Offset.Y, precision: 3);
+ }
+
+ [Fact]
+ public void Clamp_WhenCanvasIsSmallerThanViewport_CentersCanvas()
+ {
+ var state = new WhiteboardViewportState(1d, new Vector(-40, -40));
+
+ var clamped = WhiteboardViewportHelper.Clamp(
+ state,
+ new Size(300, 300),
+ new Size(100, 100));
+
+ Assert.Equal(100d, clamped.Offset.X, precision: 3);
+ Assert.Equal(100d, clamped.Offset.Y, precision: 3);
+ }
+
+ [Fact]
+ public void Clamp_AfterViewportResize_KeepsOffsetInsideBounds()
+ {
+ var state = new WhiteboardViewportState(2d, new Vector(-220, -220));
+
+ var clamped = WhiteboardViewportHelper.Clamp(
+ state,
+ new Size(300, 300),
+ new Size(200, 200));
+
+ Assert.Equal(-100d, clamped.Offset.X, precision: 3);
+ Assert.Equal(-100d, clamped.Offset.Y, precision: 3);
+ }
+}
diff --git a/LanMountainDesktop.slnx b/LanMountainDesktop.slnx
index 9c9b07f..d28439a 100644
--- a/LanMountainDesktop.slnx
+++ b/LanMountainDesktop.slnx
@@ -12,6 +12,7 @@
+
diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj
index 464bcdd..df2b6b6 100644
--- a/LanMountainDesktop/LanMountainDesktop.csproj
+++ b/LanMountainDesktop/LanMountainDesktop.csproj
@@ -52,7 +52,6 @@
All
-
@@ -80,6 +79,10 @@
+
+
+
+
diff --git a/LanMountainDesktop/Models/WhiteboardNoteSnapshot.cs b/LanMountainDesktop/Models/WhiteboardNoteSnapshot.cs
index 7aee12f..969faab 100644
--- a/LanMountainDesktop/Models/WhiteboardNoteSnapshot.cs
+++ b/LanMountainDesktop/Models/WhiteboardNoteSnapshot.cs
@@ -5,10 +5,24 @@ namespace LanMountainDesktop.Models;
public sealed class WhiteboardNoteSnapshot
{
- public int Version { get; set; } = 1;
+ public int Version { get; set; } = 2;
public DateTimeOffset SavedUtc { get; set; }
+ public DateTimeOffset? ExpiresUtc { get; set; }
+
+ public double CanvasWidth { get; set; }
+
+ public double CanvasHeight { get; set; }
+
+ public string BackgroundColor { get; set; } = "#FFFFFFFF";
+
+ public double ViewportZoom { get; set; } = 1d;
+
+ public double ViewportOffsetX { get; set; }
+
+ public double ViewportOffsetY { get; set; }
+
public List Strokes { get; set; } = [];
public WhiteboardNoteSnapshot Clone()
@@ -29,6 +43,8 @@ public sealed class WhiteboardStrokeSnapshot
public bool IgnorePressure { get; set; } = true;
+ public string? PathSvgData { get; set; }
+
public List Points { get; set; } = [];
public WhiteboardStrokeSnapshot Clone()
diff --git a/LanMountainDesktop/Services/IWhiteboardNotePersistenceService.cs b/LanMountainDesktop/Services/IWhiteboardNotePersistenceService.cs
index 44dfe8d..b02a674 100644
--- a/LanMountainDesktop/Services/IWhiteboardNotePersistenceService.cs
+++ b/LanMountainDesktop/Services/IWhiteboardNotePersistenceService.cs
@@ -7,7 +7,7 @@ public interface IWhiteboardNotePersistenceService
{
WhiteboardNoteSnapshot LoadNote(string componentId, string? placementId, int retentionDays);
- void SaveNote(string componentId, string? placementId, WhiteboardNoteSnapshot snapshot, int retentionDays);
+ bool SaveNote(string componentId, string? placementId, WhiteboardNoteSnapshot snapshot, int retentionDays);
bool DeleteNote(string componentId, string? placementId);
diff --git a/LanMountainDesktop/Services/WhiteboardNotePersistenceService.cs b/LanMountainDesktop/Services/WhiteboardNotePersistenceService.cs
index efdafdc..e68f9d3 100644
--- a/LanMountainDesktop/Services/WhiteboardNotePersistenceService.cs
+++ b/LanMountainDesktop/Services/WhiteboardNotePersistenceService.cs
@@ -1,4 +1,7 @@
using System;
+using System.IO;
+using System.Security.Cryptography;
+using System.Text;
using System.Text.Json;
using LanMountainDesktop.Models;
using Microsoft.Data.Sqlite;
@@ -8,18 +11,39 @@ namespace LanMountainDesktop.Services;
public sealed class WhiteboardNotePersistenceService : IWhiteboardNotePersistenceService
{
private const int DefaultCleanupBatchSize = 256;
+ private const int CurrentSnapshotVersion = 2;
+ private const string Category = "WhiteboardPersistence";
+
private static readonly JsonSerializerOptions JsonOptions = new()
{
- PropertyNameCaseInsensitive = true
+ PropertyNameCaseInsensitive = true,
+ WriteIndented = true
};
- private readonly object _schemaSyncRoot = new();
- private readonly AppDatabaseService _databaseService;
- private bool _schemaInitialized;
+ private readonly object _legacySchemaSyncRoot = new();
+ private readonly string _whiteboardsRootDirectory;
+ private readonly AppDatabaseService _legacyDatabaseService;
+ private bool _legacySchemaInitialized;
- public WhiteboardNotePersistenceService(AppDatabaseService? databaseService = null)
+ public WhiteboardNotePersistenceService()
+ : this(Path.Combine(AppDataPathProvider.GetDataRoot(), "Whiteboards"), AppDatabaseServiceFactory.CreateDefault())
{
- _databaseService = databaseService ?? AppDatabaseServiceFactory.CreateDefault();
+ }
+
+ public WhiteboardNotePersistenceService(AppDatabaseService? legacyDatabaseService)
+ : this(Path.Combine(AppDataPathProvider.GetDataRoot(), "Whiteboards"), legacyDatabaseService)
+ {
+ }
+
+ public WhiteboardNotePersistenceService(string whiteboardsRootDirectory, AppDatabaseService? legacyDatabaseService = null)
+ {
+ if (string.IsNullOrWhiteSpace(whiteboardsRootDirectory))
+ {
+ throw new ArgumentException("Whiteboard root directory cannot be null or whitespace.", nameof(whiteboardsRootDirectory));
+ }
+
+ _whiteboardsRootDirectory = Path.GetFullPath(whiteboardsRootDirectory);
+ _legacyDatabaseService = legacyDatabaseService ?? AppDatabaseServiceFactory.CreateDefault();
}
public WhiteboardNoteSnapshot LoadNote(string componentId, string? placementId, int retentionDays)
@@ -29,108 +53,89 @@ public sealed class WhiteboardNotePersistenceService : IWhiteboardNotePersistenc
return new WhiteboardNoteSnapshot();
}
+ var notePath = GetNoteFilePath(normalizedComponentId, normalizedPlacementId);
+ var normalizedRetentionDays = WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays);
+
try
{
- using var connection = OpenConnection();
- DeleteExpiredInternal(
- connection,
- normalizedComponentId,
- normalizedPlacementId,
- WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays),
- DateTimeOffset.UtcNow);
+ if (File.Exists(notePath))
+ {
+ var snapshot = ReadSnapshot(notePath);
+ if (IsExpired(snapshot, normalizedRetentionDays))
+ {
+ TryDeleteFile(notePath);
+ return new WhiteboardNoteSnapshot();
+ }
- using var command = connection.CreateCommand();
- command.CommandText = """
- SELECT note_json, saved_at_utc_ms
- FROM whiteboard_notes
- WHERE component_id = $componentId
- AND placement_id = $placementId
- LIMIT 1;
- """;
- command.Parameters.AddWithValue("$componentId", normalizedComponentId);
- command.Parameters.AddWithValue("$placementId", normalizedPlacementId);
+ return snapshot.Clone();
+ }
- using var reader = command.ExecuteReader();
- if (!reader.Read() || reader.IsDBNull(0))
+ var legacySnapshot = TryLoadLegacyNote(normalizedComponentId, normalizedPlacementId, normalizedRetentionDays);
+ if (legacySnapshot.Strokes.Count == 0 && legacySnapshot.SavedUtc == default)
{
return new WhiteboardNoteSnapshot();
}
- var json = reader.GetString(0);
- if (string.IsNullOrWhiteSpace(json))
+ if (!IsExpired(legacySnapshot, normalizedRetentionDays))
{
- return new WhiteboardNoteSnapshot();
+ if (SaveNote(normalizedComponentId, normalizedPlacementId, legacySnapshot, normalizedRetentionDays))
+ {
+ _ = TryDeleteLegacyNote(normalizedComponentId, normalizedPlacementId);
+ }
+
+ return legacySnapshot.Clone();
}
- var snapshot = JsonSerializer.Deserialize(json, JsonOptions) ?? new WhiteboardNoteSnapshot();
- if (!reader.IsDBNull(1))
- {
- snapshot.SavedUtc = DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64(1));
- }
-
- if (IsExpired(snapshot, retentionDays))
- {
- DeleteNote(normalizedComponentId, normalizedPlacementId);
- return new WhiteboardNoteSnapshot();
- }
-
- return snapshot.Clone();
+ _ = TryDeleteLegacyNote(normalizedComponentId, normalizedPlacementId);
}
- catch
+ catch (Exception ex)
{
- return new WhiteboardNoteSnapshot();
+ AppLogger.Warn(
+ Category,
+ $"Failed to load whiteboard note. ComponentId='{normalizedComponentId}'; PlacementId='{normalizedPlacementId}'.",
+ ex);
}
+
+ return new WhiteboardNoteSnapshot();
}
- public void SaveNote(string componentId, string? placementId, WhiteboardNoteSnapshot snapshot, int retentionDays)
+ public bool SaveNote(string componentId, string? placementId, WhiteboardNoteSnapshot snapshot, int retentionDays)
{
if (!TryNormalizeKeys(componentId, placementId, out var normalizedComponentId, out var normalizedPlacementId))
{
- return;
+ return false;
}
+ var notePath = GetNoteFilePath(normalizedComponentId, normalizedPlacementId);
+ var tempPath = $"{notePath}.{Guid.NewGuid():N}.tmp";
+
try
{
var nowUtc = DateTimeOffset.UtcNow;
var persistedSnapshot = snapshot?.Clone() ?? new WhiteboardNoteSnapshot();
+ persistedSnapshot.Version = CurrentSnapshotVersion;
persistedSnapshot.SavedUtc = nowUtc;
- var expiresUtc = GetExpirationUtc(persistedSnapshot, retentionDays) ?? nowUtc.AddDays(WhiteboardNoteRetentionPolicy.DefaultDays);
- var json = JsonSerializer.Serialize(persistedSnapshot, JsonOptions);
+ persistedSnapshot.ExpiresUtc = nowUtc.AddDays(WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays));
- using var connection = OpenConnection();
- using var command = connection.CreateCommand();
- command.CommandText = """
- INSERT INTO whiteboard_notes(
- component_id,
- placement_id,
- note_json,
- saved_at_utc_ms,
- expires_at_utc_ms,
- updated_at_utc_ms)
- VALUES(
- $componentId,
- $placementId,
- $noteJson,
- $savedAtUtcMs,
- $expiresAtUtcMs,
- $updatedAtUtcMs)
- ON CONFLICT(component_id, placement_id) DO UPDATE SET
- note_json = excluded.note_json,
- saved_at_utc_ms = excluded.saved_at_utc_ms,
- expires_at_utc_ms = excluded.expires_at_utc_ms,
- updated_at_utc_ms = excluded.updated_at_utc_ms;
- """;
- command.Parameters.AddWithValue("$componentId", normalizedComponentId);
- command.Parameters.AddWithValue("$placementId", normalizedPlacementId);
- command.Parameters.AddWithValue("$noteJson", json);
- command.Parameters.AddWithValue("$savedAtUtcMs", persistedSnapshot.SavedUtc.ToUnixTimeMilliseconds());
- command.Parameters.AddWithValue("$expiresAtUtcMs", expiresUtc.ToUnixTimeMilliseconds());
- command.Parameters.AddWithValue("$updatedAtUtcMs", nowUtc.ToUnixTimeMilliseconds());
- command.ExecuteNonQuery();
+ var directory = Path.GetDirectoryName(notePath);
+ if (!string.IsNullOrWhiteSpace(directory))
+ {
+ Directory.CreateDirectory(directory);
+ }
+
+ var json = JsonSerializer.Serialize(persistedSnapshot, JsonOptions);
+ File.WriteAllText(tempPath, json, Encoding.UTF8);
+ File.Move(tempPath, notePath, overwrite: true);
+ return true;
}
- catch
+ catch (Exception ex)
{
- // Keep whiteboard usable even when persistence is unavailable.
+ TryDeleteFile(tempPath);
+ AppLogger.Warn(
+ Category,
+ $"Failed to save whiteboard note. ComponentId='{normalizedComponentId}'; PlacementId='{normalizedPlacementId}'; StrokeCount={snapshot?.Strokes.Count ?? 0}.",
+ ex);
+ return false;
}
}
@@ -141,23 +146,9 @@ public sealed class WhiteboardNotePersistenceService : IWhiteboardNotePersistenc
return false;
}
- try
- {
- using var connection = OpenConnection();
- using var command = connection.CreateCommand();
- command.CommandText = """
- DELETE FROM whiteboard_notes
- WHERE component_id = $componentId
- AND placement_id = $placementId;
- """;
- command.Parameters.AddWithValue("$componentId", normalizedComponentId);
- command.Parameters.AddWithValue("$placementId", normalizedPlacementId);
- return command.ExecuteNonQuery() > 0;
- }
- catch
- {
- return false;
- }
+ var deleted = TryDeleteFile(GetNoteFilePath(normalizedComponentId, normalizedPlacementId));
+ deleted |= TryDeleteLegacyNote(normalizedComponentId, normalizedPlacementId);
+ return deleted;
}
public bool TryDeleteExpiredNote(string componentId, string? placementId, int retentionDays)
@@ -167,46 +158,72 @@ public sealed class WhiteboardNotePersistenceService : IWhiteboardNotePersistenc
return false;
}
+ var notePath = GetNoteFilePath(normalizedComponentId, normalizedPlacementId);
try
{
- using var connection = OpenConnection();
- return DeleteExpiredInternal(
- connection,
- normalizedComponentId,
- normalizedPlacementId,
- WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays),
- DateTimeOffset.UtcNow);
+ if (File.Exists(notePath))
+ {
+ var snapshot = ReadSnapshot(notePath);
+ if (IsExpired(snapshot, retentionDays))
+ {
+ return TryDeleteFile(notePath);
+ }
+
+ return false;
+ }
+
+ return TryDeleteExpiredLegacyNote(normalizedComponentId, normalizedPlacementId, retentionDays);
}
- catch
+ catch (Exception ex)
{
+ AppLogger.Warn(
+ Category,
+ $"Failed to delete expired whiteboard note. ComponentId='{normalizedComponentId}'; PlacementId='{normalizedPlacementId}'.",
+ ex);
return false;
}
}
public int DeleteExpiredNotesBatch(int batchSize = DefaultCleanupBatchSize, DateTimeOffset? now = null)
{
+ var deletedCount = 0;
+ var normalizedBatchSize = NormalizeBatchSize(batchSize);
+ var nowUtc = now ?? DateTimeOffset.UtcNow;
+
try
{
- using var connection = OpenConnection();
- using var command = connection.CreateCommand();
- command.CommandText = """
- DELETE FROM whiteboard_notes
- WHERE rowid IN (
- SELECT rowid
- FROM whiteboard_notes
- WHERE expires_at_utc_ms <= $nowUtcMs
- ORDER BY expires_at_utc_ms ASC
- LIMIT $batchSize
- );
- """;
- command.Parameters.AddWithValue("$nowUtcMs", (now ?? DateTimeOffset.UtcNow).ToUnixTimeMilliseconds());
- command.Parameters.AddWithValue("$batchSize", NormalizeBatchSize(batchSize));
- return command.ExecuteNonQuery();
+ if (Directory.Exists(_whiteboardsRootDirectory))
+ {
+ foreach (var notePath in Directory.EnumerateFiles(_whiteboardsRootDirectory, "*.json", SearchOption.AllDirectories))
+ {
+ if (deletedCount >= normalizedBatchSize)
+ {
+ break;
+ }
+
+ try
+ {
+ var snapshot = ReadSnapshot(notePath);
+ if (IsExpired(snapshot, WhiteboardNoteRetentionPolicy.DefaultDays, nowUtc) &&
+ TryDeleteFile(notePath))
+ {
+ deletedCount++;
+ }
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn(Category, $"Failed to inspect whiteboard note file '{notePath}'.", ex);
+ }
+ }
+ }
}
- catch
+ catch (Exception ex)
{
- return 0;
+ AppLogger.Warn(Category, $"Failed to scan whiteboard note directory '{_whiteboardsRootDirectory}'.", ex);
}
+
+ deletedCount += DeleteExpiredLegacyNotesBatch(Math.Max(0, normalizedBatchSize - deletedCount), nowUtc);
+ return deletedCount;
}
public bool IsExpired(WhiteboardNoteSnapshot snapshot, int retentionDays, DateTimeOffset? now = null)
@@ -227,7 +244,17 @@ public sealed class WhiteboardNotePersistenceService : IWhiteboardNotePersistenc
public DateTimeOffset? GetExpirationUtc(WhiteboardNoteSnapshot snapshot, int retentionDays)
{
- if (snapshot is null || snapshot.SavedUtc == default)
+ if (snapshot is null)
+ {
+ return null;
+ }
+
+ if (snapshot.ExpiresUtc.HasValue)
+ {
+ return snapshot.ExpiresUtc.Value;
+ }
+
+ if (snapshot.SavedUtc == default)
{
return null;
}
@@ -235,23 +262,170 @@ public sealed class WhiteboardNotePersistenceService : IWhiteboardNotePersistenc
return snapshot.SavedUtc.AddDays(WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays));
}
- private SqliteConnection OpenConnection()
+ internal string GetNoteFilePathForTests(string componentId, string? placementId)
{
- var connection = _databaseService.OpenConnection();
- EnsureSchema(connection);
+ if (!TryNormalizeKeys(componentId, placementId, out var normalizedComponentId, out var normalizedPlacementId))
+ {
+ return string.Empty;
+ }
+
+ return GetNoteFilePath(normalizedComponentId, normalizedPlacementId);
+ }
+
+ private string GetNoteFilePath(string normalizedComponentId, string normalizedPlacementId)
+ {
+ return Path.Combine(
+ _whiteboardsRootDirectory,
+ SanitizePathSegment(normalizedComponentId),
+ $"{SanitizePathSegment(normalizedPlacementId)}.json");
+ }
+
+ private static WhiteboardNoteSnapshot ReadSnapshot(string notePath)
+ {
+ var json = File.ReadAllText(notePath, Encoding.UTF8);
+ if (string.IsNullOrWhiteSpace(json))
+ {
+ return new WhiteboardNoteSnapshot();
+ }
+
+ return JsonSerializer.Deserialize(json, JsonOptions) ?? new WhiteboardNoteSnapshot();
+ }
+
+ private WhiteboardNoteSnapshot TryLoadLegacyNote(string componentId, string placementId, int retentionDays)
+ {
+ try
+ {
+ using var connection = OpenLegacyConnection();
+ TryDeleteExpiredLegacyNote(connection, componentId, placementId, retentionDays, DateTimeOffset.UtcNow);
+
+ using var command = connection.CreateCommand();
+ command.CommandText = """
+ SELECT note_json, saved_at_utc_ms, expires_at_utc_ms
+ FROM whiteboard_notes
+ WHERE component_id = $componentId
+ AND placement_id = $placementId
+ LIMIT 1;
+ """;
+ command.Parameters.AddWithValue("$componentId", componentId);
+ command.Parameters.AddWithValue("$placementId", placementId);
+
+ using var reader = command.ExecuteReader();
+ if (!reader.Read() || reader.IsDBNull(0))
+ {
+ return new WhiteboardNoteSnapshot();
+ }
+
+ var snapshot = JsonSerializer.Deserialize(reader.GetString(0), JsonOptions) ??
+ new WhiteboardNoteSnapshot();
+ if (!reader.IsDBNull(1))
+ {
+ snapshot.SavedUtc = DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64(1));
+ }
+
+ if (!reader.IsDBNull(2))
+ {
+ snapshot.ExpiresUtc = DateTimeOffset.FromUnixTimeMilliseconds(reader.GetInt64(2));
+ }
+
+ return snapshot;
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn(
+ Category,
+ $"Failed to load legacy whiteboard note. ComponentId='{componentId}'; PlacementId='{placementId}'.",
+ ex);
+ return new WhiteboardNoteSnapshot();
+ }
+ }
+
+ private bool TryDeleteLegacyNote(string componentId, string placementId)
+ {
+ try
+ {
+ using var connection = OpenLegacyConnection();
+ using var command = connection.CreateCommand();
+ command.CommandText = """
+ DELETE FROM whiteboard_notes
+ WHERE component_id = $componentId
+ AND placement_id = $placementId;
+ """;
+ command.Parameters.AddWithValue("$componentId", componentId);
+ command.Parameters.AddWithValue("$placementId", placementId);
+ return command.ExecuteNonQuery() > 0;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private bool TryDeleteExpiredLegacyNote(string componentId, string placementId, int retentionDays)
+ {
+ try
+ {
+ using var connection = OpenLegacyConnection();
+ return TryDeleteExpiredLegacyNote(
+ connection,
+ componentId,
+ placementId,
+ WhiteboardNoteRetentionPolicy.NormalizeDays(retentionDays),
+ DateTimeOffset.UtcNow);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private int DeleteExpiredLegacyNotesBatch(int batchSize, DateTimeOffset nowUtc)
+ {
+ if (batchSize <= 0)
+ {
+ return 0;
+ }
+
+ try
+ {
+ using var connection = OpenLegacyConnection();
+ using var command = connection.CreateCommand();
+ command.CommandText = """
+ DELETE FROM whiteboard_notes
+ WHERE rowid IN (
+ SELECT rowid
+ FROM whiteboard_notes
+ WHERE expires_at_utc_ms <= $nowUtcMs
+ ORDER BY expires_at_utc_ms ASC
+ LIMIT $batchSize
+ );
+ """;
+ command.Parameters.AddWithValue("$nowUtcMs", nowUtc.ToUnixTimeMilliseconds());
+ command.Parameters.AddWithValue("$batchSize", NormalizeBatchSize(batchSize));
+ return command.ExecuteNonQuery();
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+
+ private SqliteConnection OpenLegacyConnection()
+ {
+ var connection = _legacyDatabaseService.OpenConnection();
+ EnsureLegacySchema(connection);
return connection;
}
- private void EnsureSchema(SqliteConnection connection)
+ private void EnsureLegacySchema(SqliteConnection connection)
{
- if (_schemaInitialized)
+ if (_legacySchemaInitialized)
{
return;
}
- lock (_schemaSyncRoot)
+ lock (_legacySchemaSyncRoot)
{
- if (_schemaInitialized)
+ if (_legacySchemaInitialized)
{
return;
}
@@ -272,11 +446,11 @@ public sealed class WhiteboardNotePersistenceService : IWhiteboardNotePersistenc
ON whiteboard_notes(expires_at_utc_ms);
""";
command.ExecuteNonQuery();
- _schemaInitialized = true;
+ _legacySchemaInitialized = true;
}
}
- private static bool DeleteExpiredInternal(
+ private static bool TryDeleteExpiredLegacyNote(
SqliteConnection connection,
string componentId,
string placementId,
@@ -326,7 +500,51 @@ public sealed class WhiteboardNotePersistenceService : IWhiteboardNotePersistenc
{
normalizedComponentId = componentId?.Trim() ?? string.Empty;
normalizedPlacementId = placementId?.Trim() ?? string.Empty;
- return !string.IsNullOrWhiteSpace(normalizedComponentId);
+ return !string.IsNullOrWhiteSpace(normalizedComponentId) &&
+ !string.IsNullOrWhiteSpace(normalizedPlacementId);
+ }
+
+ private static string SanitizePathSegment(string value)
+ {
+ var invalidChars = Path.GetInvalidFileNameChars();
+ var builder = new StringBuilder(value.Length);
+ foreach (var ch in value.Trim())
+ {
+ builder.Append(Array.IndexOf(invalidChars, ch) >= 0 ? '_' : ch);
+ }
+
+ var safe = builder.ToString();
+ if (string.IsNullOrWhiteSpace(safe))
+ {
+ return "_";
+ }
+
+ if (safe.Length <= 120)
+ {
+ return safe;
+ }
+
+ var hash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(safe)))[..12].ToLowerInvariant();
+ return $"{safe[..100]}-{hash}";
+ }
+
+ private static bool TryDeleteFile(string path)
+ {
+ try
+ {
+ if (!string.IsNullOrWhiteSpace(path) && File.Exists(path))
+ {
+ File.SetAttributes(path, FileAttributes.Normal);
+ File.Delete(path);
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn(Category, $"Failed to delete whiteboard note file '{path}'.", ex);
+ }
+
+ return false;
}
private static int NormalizeBatchSize(int batchSize)
diff --git a/LanMountainDesktop/Services/WhiteboardSvgImportService.cs b/LanMountainDesktop/Services/WhiteboardSvgImportService.cs
new file mode 100644
index 0000000..c9d83e7
--- /dev/null
+++ b/LanMountainDesktop/Services/WhiteboardSvgImportService.cs
@@ -0,0 +1,321 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Xml.Linq;
+using Avalonia.Media;
+using LanMountainDesktop.Models;
+using SkiaSharp;
+
+namespace LanMountainDesktop.Services;
+
+public sealed class WhiteboardSvgImportResult
+{
+ public List Strokes { get; init; } = [];
+
+ public int SkippedPathCount { get; init; }
+}
+
+public static class WhiteboardSvgImportService
+{
+ public static WhiteboardSvgImportResult Import(Stream stream, double targetWidth, double targetHeight)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+
+ var document = XDocument.Load(stream);
+ var root = document.Root;
+ if (root is null)
+ {
+ return new WhiteboardSvgImportResult();
+ }
+
+ var viewport = ResolveViewport(root);
+ var transform = ResolveTransform(viewport, targetWidth, targetHeight);
+ var importedStrokes = new List();
+ var skippedPathCount = 0;
+
+ foreach (var pathElement in root.Descendants().Where(static element =>
+ string.Equals(element.Name.LocalName, "path", StringComparison.OrdinalIgnoreCase)))
+ {
+ var pathData = pathElement.Attribute("d")?.Value;
+ if (string.IsNullOrWhiteSpace(pathData))
+ {
+ skippedPathCount++;
+ continue;
+ }
+
+ using var parsedPath = SKPath.ParseSvgPathData(pathData);
+ if (parsedPath is null || parsedPath.IsEmpty)
+ {
+ skippedPathCount++;
+ continue;
+ }
+
+ using var transformedPath = new SKPath(parsedPath);
+ transformedPath.Transform(transform);
+
+ var style = ParseStyle(pathElement.Attribute("style")?.Value);
+ var fillValue = ResolvePresentationValue(pathElement, style, "fill");
+ var strokeValue = ResolvePresentationValue(pathElement, style, "stroke");
+ var strokeWidth = ResolveStrokeWidth(pathElement, style) * ResolveStrokeScale(transform);
+
+ if (IsNone(fillValue) && TryParseSvgColor(strokeValue, out var strokeColor))
+ {
+ using var filledStrokePath = StrokePathToFillPath(transformedPath, strokeWidth);
+ if (filledStrokePath.IsEmpty)
+ {
+ skippedPathCount++;
+ continue;
+ }
+
+ importedStrokes.Add(CreateSnapshot(filledStrokePath, strokeColor, strokeWidth));
+ continue;
+ }
+
+ if (!TryParseSvgColor(fillValue, out var fillColor) &&
+ !TryParseSvgColor(strokeValue, out fillColor))
+ {
+ fillColor = SKColors.Black;
+ }
+
+ importedStrokes.Add(CreateSnapshot(transformedPath, fillColor, Math.Max(1d, strokeWidth)));
+ }
+
+ return new WhiteboardSvgImportResult
+ {
+ Strokes = importedStrokes,
+ SkippedPathCount = skippedPathCount
+ };
+ }
+
+ private static WhiteboardStrokeSnapshot CreateSnapshot(SKPath path, SKColor color, double inkThickness)
+ {
+ return new WhiteboardStrokeSnapshot
+ {
+ Color = ToHexColor(color),
+ InkThickness = Math.Max(0.5d, inkThickness),
+ IgnorePressure = true,
+ PathSvgData = path.ToSvgPathData()
+ };
+ }
+
+ private static SKPath StrokePathToFillPath(SKPath sourcePath, double strokeWidth)
+ {
+ var fillPath = new SKPath();
+ using var paint = new SKPaint
+ {
+ IsAntialias = true,
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = Math.Max(0.5f, (float)strokeWidth),
+ StrokeCap = SKStrokeCap.Round,
+ StrokeJoin = SKStrokeJoin.Round
+ };
+
+ if (!paint.GetFillPath(sourcePath, fillPath))
+ {
+ fillPath.Reset();
+ }
+
+ return fillPath;
+ }
+
+ private static SvgViewport ResolveViewport(XElement root)
+ {
+ var viewBox = root.Attribute("viewBox")?.Value;
+ if (!string.IsNullOrWhiteSpace(viewBox))
+ {
+ var parts = viewBox
+ .Split([' ', ','], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
+ .Select(ParseSvgLength)
+ .ToArray();
+ if (parts.Length == 4 && parts[2] > 0 && parts[3] > 0)
+ {
+ return new SvgViewport(parts[0], parts[1], parts[2], parts[3]);
+ }
+ }
+
+ var width = ParseSvgLength(root.Attribute("width")?.Value);
+ var height = ParseSvgLength(root.Attribute("height")?.Value);
+ return new SvgViewport(0d, 0d, Math.Max(1d, width), Math.Max(1d, height));
+ }
+
+ private static SKMatrix ResolveTransform(SvgViewport viewport, double targetWidth, double targetHeight)
+ {
+ var scaleX = targetWidth > 0 ? targetWidth / viewport.Width : 1d;
+ var scaleY = targetHeight > 0 ? targetHeight / viewport.Height : 1d;
+ return new SKMatrix
+ {
+ ScaleX = (float)scaleX,
+ SkewX = 0f,
+ TransX = (float)(-viewport.X * scaleX),
+ SkewY = 0f,
+ ScaleY = (float)scaleY,
+ TransY = (float)(-viewport.Y * scaleY),
+ Persp0 = 0f,
+ Persp1 = 0f,
+ Persp2 = 1f
+ };
+ }
+
+ private static double ResolveStrokeScale(SKMatrix transform)
+ {
+ return Math.Max(0.01d, (Math.Abs(transform.ScaleX) + Math.Abs(transform.ScaleY)) * 0.5d);
+ }
+
+ private static Dictionary ParseStyle(string? value)
+ {
+ var style = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return style;
+ }
+
+ foreach (var declaration in value.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
+ {
+ var separatorIndex = declaration.IndexOf(':', StringComparison.Ordinal);
+ if (separatorIndex <= 0 || separatorIndex >= declaration.Length - 1)
+ {
+ continue;
+ }
+
+ style[declaration[..separatorIndex].Trim()] = declaration[(separatorIndex + 1)..].Trim();
+ }
+
+ return style;
+ }
+
+ private static string? ResolvePresentationValue(
+ XElement pathElement,
+ IReadOnlyDictionary style,
+ string key)
+ {
+ if (pathElement.Attribute(key)?.Value is { } attributeValue)
+ {
+ return attributeValue;
+ }
+
+ return style.TryGetValue(key, out var styleValue) ? styleValue : null;
+ }
+
+ private static double ResolveStrokeWidth(XElement pathElement, IReadOnlyDictionary style)
+ {
+ var value = ResolvePresentationValue(pathElement, style, "stroke-width");
+ var parsed = ParseSvgLength(value);
+ return parsed > 0 ? parsed : 1d;
+ }
+
+ private static double ParseSvgLength(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return 0d;
+ }
+
+ var trimmed = value.Trim();
+ var end = 0;
+ while (end < trimmed.Length &&
+ (char.IsDigit(trimmed[end]) ||
+ trimmed[end] is '.' or '-' or '+' or 'e' or 'E'))
+ {
+ end++;
+ }
+
+ return double.TryParse(
+ trimmed[..end],
+ NumberStyles.Float,
+ CultureInfo.InvariantCulture,
+ out var parsed)
+ ? parsed
+ : 0d;
+ }
+
+ private static bool IsNone(string? value)
+ {
+ return string.Equals(value?.Trim(), "none", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static bool TryParseSvgColor(string? value, out SKColor color)
+ {
+ color = SKColors.Black;
+ if (string.IsNullOrWhiteSpace(value) || IsNone(value))
+ {
+ return false;
+ }
+
+ var trimmed = value.Trim();
+ if (string.Equals(trimmed, "transparent", StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ if (TryParseShortHexColor(trimmed, out color))
+ {
+ return true;
+ }
+
+ try
+ {
+ var avaloniaColor = Color.Parse(trimmed);
+ color = new SKColor(avaloniaColor.R, avaloniaColor.G, avaloniaColor.B, avaloniaColor.A);
+ return color.Alpha > 0;
+ }
+ catch
+ {
+ return TryParseNamedColor(trimmed, out color);
+ }
+ }
+
+ private static bool TryParseShortHexColor(string value, out SKColor color)
+ {
+ color = SKColors.Black;
+ if (!value.StartsWith('#') || value.Length is not (4 or 5))
+ {
+ return false;
+ }
+
+ static byte Expand(char ch)
+ {
+ var value = Convert.ToByte(ch.ToString(), 16);
+ return (byte)((value << 4) | value);
+ }
+
+ try
+ {
+ var r = Expand(value[1]);
+ var g = Expand(value[2]);
+ var b = Expand(value[3]);
+ var a = value.Length == 5 ? Expand(value[4]) : (byte)255;
+ color = new SKColor(r, g, b, a);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ private static bool TryParseNamedColor(string value, out SKColor color)
+ {
+ color = value.Trim().ToLowerInvariant() switch
+ {
+ "black" => SKColors.Black,
+ "white" => SKColors.White,
+ "red" => SKColors.Red,
+ "green" => SKColors.Green,
+ "blue" => SKColors.Blue,
+ "yellow" => SKColors.Yellow,
+ "gray" or "grey" => SKColors.Gray,
+ _ => default
+ };
+
+ return color != default;
+ }
+
+ private static string ToHexColor(SKColor color)
+ {
+ return $"#{color.Alpha:X2}{color.Red:X2}{color.Green:X2}{color.Blue:X2}";
+ }
+
+ private readonly record struct SvgViewport(double X, double Y, double Width, double Height);
+}
diff --git a/LanMountainDesktop/Views/Components/WhiteboardStrokePathBuilder.cs b/LanMountainDesktop/Views/Components/WhiteboardStrokePathBuilder.cs
new file mode 100644
index 0000000..537bb13
--- /dev/null
+++ b/LanMountainDesktop/Views/Components/WhiteboardStrokePathBuilder.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Collections.Generic;
+using DotNetCampus.Inking.Primitive;
+using SkiaSharp;
+
+namespace LanMountainDesktop.Views.Components;
+
+internal static class WhiteboardStrokePathBuilder
+{
+ private const float MinimumInkThickness = 0.5f;
+ private const float PointOverlapTolerance = 0.01f;
+
+ public static SKPath BuildPath(IReadOnlyList pointList, double inkThickness)
+ {
+ var strokePath = new SKPath();
+ if (pointList.Count == 0)
+ {
+ return strokePath;
+ }
+
+ var points = CollectFinitePoints(pointList);
+ if (points.Count == 0)
+ {
+ return strokePath;
+ }
+
+ var normalizedThickness = NormalizeInkThickness(inkThickness);
+ if (points.Count == 1 || AreAllPointsOverlapping(points))
+ {
+ AddSinglePointStroke(strokePath, points[0], normalizedThickness);
+ return strokePath;
+ }
+
+ using var centerPath = BuildCenterPath(points);
+ using var strokePaint = new SKPaint
+ {
+ IsAntialias = true,
+ Style = SKPaintStyle.Stroke,
+ StrokeWidth = normalizedThickness,
+ StrokeCap = SKStrokeCap.Round,
+ StrokeJoin = SKStrokeJoin.Round
+ };
+
+ if (!strokePaint.GetFillPath(centerPath, strokePath) || strokePath.IsEmpty)
+ {
+ strokePath.Reset();
+ AddPointFallbackStroke(strokePath, points, normalizedThickness);
+ }
+
+ return strokePath;
+ }
+
+ private static List CollectFinitePoints(IReadOnlyList pointList)
+ {
+ var points = new List(pointList.Count);
+ foreach (var point in pointList)
+ {
+ if (!double.IsFinite(point.X) || !double.IsFinite(point.Y))
+ {
+ continue;
+ }
+
+ points.Add(new SKPoint((float)point.X, (float)point.Y));
+ }
+
+ return points;
+ }
+
+ private static SKPath BuildCenterPath(IReadOnlyList points)
+ {
+ var centerPath = new SKPath();
+ centerPath.MoveTo(points[0].X, points[0].Y);
+
+ if (points.Count == 2)
+ {
+ centerPath.LineTo(points[1].X, points[1].Y);
+ return centerPath;
+ }
+
+ for (var i = 1; i < points.Count - 1; i++)
+ {
+ var current = points[i];
+ var next = points[i + 1];
+ var midpoint = new SKPoint(
+ (current.X + next.X) * 0.5f,
+ (current.Y + next.Y) * 0.5f);
+ centerPath.QuadTo(current.X, current.Y, midpoint.X, midpoint.Y);
+ }
+
+ var lastPoint = points[^1];
+ centerPath.LineTo(lastPoint.X, lastPoint.Y);
+ return centerPath;
+ }
+
+ private static void AddPointFallbackStroke(SKPath path, IReadOnlyList points, float inkThickness)
+ {
+ foreach (var point in points)
+ {
+ AddSinglePointStroke(path, point, inkThickness);
+ }
+ }
+
+ private static void AddSinglePointStroke(SKPath path, SKPoint point, float inkThickness)
+ {
+ path.AddCircle(point.X, point.Y, inkThickness * 0.5f);
+ }
+
+ private static bool AreAllPointsOverlapping(IReadOnlyList points)
+ {
+ var firstPoint = points[0];
+ for (var i = 1; i < points.Count; i++)
+ {
+ if (Math.Abs(points[i].X - firstPoint.X) > PointOverlapTolerance ||
+ Math.Abs(points[i].Y - firstPoint.Y) > PointOverlapTolerance)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static float NormalizeInkThickness(double inkThickness)
+ {
+ if (!double.IsFinite(inkThickness))
+ {
+ return MinimumInkThickness;
+ }
+
+ return Math.Max(MinimumInkThickness, (float)inkThickness);
+ }
+}
diff --git a/LanMountainDesktop/Views/Components/WhiteboardViewportHelper.cs b/LanMountainDesktop/Views/Components/WhiteboardViewportHelper.cs
new file mode 100644
index 0000000..adabe2d
--- /dev/null
+++ b/LanMountainDesktop/Views/Components/WhiteboardViewportHelper.cs
@@ -0,0 +1,137 @@
+using System;
+using Avalonia;
+
+namespace LanMountainDesktop.Views.Components;
+
+internal readonly record struct WhiteboardViewportState(double Zoom, Vector Offset);
+
+internal static class WhiteboardViewportHelper
+{
+ public const double DefaultZoom = 1d;
+ public const double MinZoom = 0.5d;
+ public const double MaxZoom = 4d;
+
+ public static WhiteboardViewportState CreateDefault(Size viewportSize, Size canvasSize)
+ {
+ return Clamp(new WhiteboardViewportState(DefaultZoom, default), viewportSize, canvasSize);
+ }
+
+ public static WhiteboardViewportState Clamp(
+ WhiteboardViewportState state,
+ Size viewportSize,
+ Size canvasSize)
+ {
+ var zoom = NormalizeZoom(state.Zoom);
+ var viewport = NormalizeSize(viewportSize);
+ var canvas = NormalizeSize(canvasSize);
+ var scaledWidth = canvas.Width * zoom;
+ var scaledHeight = canvas.Height * zoom;
+
+ return new WhiteboardViewportState(
+ zoom,
+ new Vector(
+ ClampAxis(state.Offset.X, viewport.Width, scaledWidth),
+ ClampAxis(state.Offset.Y, viewport.Height, scaledHeight)));
+ }
+
+ public static WhiteboardViewportState PanBy(
+ WhiteboardViewportState state,
+ Vector delta,
+ Size viewportSize,
+ Size canvasSize)
+ {
+ if (!IsFinite(delta.X) || !IsFinite(delta.Y))
+ {
+ return Clamp(state, viewportSize, canvasSize);
+ }
+
+ return Clamp(state with { Offset = state.Offset + delta }, viewportSize, canvasSize);
+ }
+
+ public static WhiteboardViewportState ZoomAt(
+ WhiteboardViewportState state,
+ double targetZoom,
+ Point anchorViewportPoint,
+ Size viewportSize,
+ Size canvasSize)
+ {
+ return ZoomFromGestureStart(
+ state,
+ targetZoom,
+ anchorViewportPoint,
+ anchorViewportPoint,
+ viewportSize,
+ canvasSize);
+ }
+
+ public static WhiteboardViewportState ZoomFromGestureStart(
+ WhiteboardViewportState startState,
+ double targetZoom,
+ Point initialCenter,
+ Point currentCenter,
+ Size viewportSize,
+ Size canvasSize)
+ {
+ var start = Clamp(startState, viewportSize, canvasSize);
+ var zoom = NormalizeZoom(targetZoom);
+ var anchor = ToLogicalPoint(start, initialCenter);
+ var offset = new Vector(
+ currentCenter.X - (anchor.X * zoom),
+ currentCenter.Y - (anchor.Y * zoom));
+
+ return Clamp(new WhiteboardViewportState(zoom, offset), viewportSize, canvasSize);
+ }
+
+ public static Point ToLogicalPoint(WhiteboardViewportState state, Point viewportPoint)
+ {
+ var zoom = NormalizeZoom(state.Zoom);
+ return new Point(
+ (viewportPoint.X - state.Offset.X) / zoom,
+ (viewportPoint.Y - state.Offset.Y) / zoom);
+ }
+
+ public static WhiteboardViewportState Fit(Size viewportSize, Size canvasSize)
+ {
+ var viewport = NormalizeSize(viewportSize);
+ var canvas = NormalizeSize(canvasSize);
+ var fitZoom = Math.Min(viewport.Width / canvas.Width, viewport.Height / canvas.Height);
+ return Clamp(new WhiteboardViewportState(fitZoom, default), viewport, canvas);
+ }
+
+ public static double NormalizeZoom(double zoom)
+ {
+ if (!IsFinite(zoom))
+ {
+ return DefaultZoom;
+ }
+
+ return Math.Clamp(zoom, MinZoom, MaxZoom);
+ }
+
+ public static Size NormalizeSize(Size size)
+ {
+ return new Size(
+ IsFinite(size.Width) ? Math.Max(1d, size.Width) : 1d,
+ IsFinite(size.Height) ? Math.Max(1d, size.Height) : 1d);
+ }
+
+ private static double ClampAxis(double offset, double viewportLength, double scaledCanvasLength)
+ {
+ if (!IsFinite(offset))
+ {
+ offset = 0d;
+ }
+
+ if (scaledCanvasLength <= viewportLength)
+ {
+ return (viewportLength - scaledCanvasLength) * 0.5d;
+ }
+
+ return Math.Clamp(offset, viewportLength - scaledCanvasLength, 0d);
+ }
+
+ private static bool IsFinite(double value)
+ {
+ return !double.IsNaN(value) && !double.IsInfinity(value);
+ }
+}
diff --git a/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml b/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml
index e884d59..6990cd8 100644
--- a/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml
+++ b/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml
@@ -26,7 +26,21 @@
BorderThickness="1"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
ClipToBounds="True">
-
+
+
+
+
+
-
diff --git a/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml.cs b/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml.cs
index ca2ad30..2b5a627 100644
--- a/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/WhiteboardWidget.axaml.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
+using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Platform.Storage;
@@ -27,17 +28,31 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
private enum WhiteboardToolMode
{
Pen,
- Eraser
+ Eraser,
+ PanZoom
}
- private static readonly PropertyInfo? StrokeColorProperty = typeof(SkiaStroke).GetProperty(nameof(SkiaStroke.Color));
- private static readonly PropertyInfo? StrokePointListProperty = typeof(SkiaStroke).GetProperty("PointList");
+ private readonly record struct PanZoomGestureBaseline(
+ WhiteboardViewportState StartViewport,
+ Point InitialCenter,
+ double InitialDistance);
+
+ private static readonly BindingFlags StrokeReflectionFlags =
+ BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
+ private static readonly PropertyInfo? StrokeColorProperty = typeof(SkiaStroke).GetProperty(nameof(SkiaStroke.Color), StrokeReflectionFlags);
+ private static readonly PropertyInfo? StrokePointListProperty = typeof(SkiaStroke).GetProperty("PointList", StrokeReflectionFlags);
private readonly int _baseWidthCells;
private readonly IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly IWhiteboardNotePersistenceService _notePersistenceService = new WhiteboardNotePersistenceService();
- private readonly DispatcherTimer _noteSaveTimer = new() { Interval = TimeSpan.FromMinutes(5) };
+ private readonly DispatcherTimer _noteSaveTimer = new() { Interval = TimeSpan.FromSeconds(2) };
+ private readonly Dictionary _panZoomPointers = [];
+ private readonly ScaleTransform _viewportScaleTransform = new();
+ private readonly TranslateTransform _viewportTranslateTransform = new();
private double _currentCellSize = 48;
private WhiteboardToolMode _toolMode = WhiteboardToolMode.Pen;
+ private Size _logicalCanvasSize = new(1, 1);
+ private WhiteboardViewportState _viewportState = new(WhiteboardViewportHelper.DefaultZoom, default);
+ private PanZoomGestureBaseline? _panZoomGestureBaseline;
private bool? _isNightModeApplied;
private SKColor _selectedInkColor = SKColors.Black;
private bool _isUserCustomColor;
@@ -47,6 +62,7 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
private int _noteRetentionDays = WhiteboardNoteRetentionPolicy.DefaultDays;
private bool _isApplyingPersistedSnapshot;
private bool _noteDirty;
+ private int _noteSaveRevision;
private int _noteLoadRevision;
private bool _disposed;
@@ -59,6 +75,14 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
{
_baseWidthCells = Math.Max(1, baseWidthCells);
InitializeComponent();
+ InkCanvas.RenderTransform = new TransformGroup
+ {
+ Children =
+ {
+ _viewportScaleTransform,
+ _viewportTranslateTransform
+ }
+ };
AttachedToVisualTree += OnAttachedToVisualTree;
DetachedFromVisualTree += OnDetachedFromVisualTree;
SizeChanged += OnSizeChanged;
@@ -66,7 +90,10 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
_noteSaveTimer.Tick += OnNoteSaveTimerTick;
ConfigureInkCanvas();
+ ConfigureViewportGestures();
ApplyCellSize(_currentCellSize);
+ EnsureLogicalCanvasSize(expandToViewport: true);
+ ApplyViewportTransform();
RefreshFromSettings();
ApplyThemeVisual(force: true);
InitializeColorPicker();
@@ -95,6 +122,8 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
ApplyThemeVisual(force: true);
+ EnsureLogicalCanvasSize(expandToViewport: true);
+ SetViewportState(_viewportState, queueSave: false);
SchedulePersistedNoteLoad();
}
@@ -106,6 +135,8 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
ApplyCellSize(_currentCellSize);
+ EnsureLogicalCanvasSize(expandToViewport: true);
+ SetViewportState(_viewportState, queueSave: false);
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
@@ -120,13 +151,24 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
settings.IgnorePressure = true;
settings.InkThickness = _selectedInkThickness;
settings.EraserSize = new Size(20, 20);
- settings.IsBitmapCacheEnabled = true;
- settings.MaxBitmapCacheSize = 2048;
+ settings.IsBitmapCacheEnabled = false;
InkCanvas.StrokeCollected += OnInkCanvasStrokeCollected;
+ InkCanvas.StrokeErased += OnInkCanvasStrokeErased;
InkCanvas.PointerReleased += OnInkCanvasPointerReleased;
InkCanvas.PointerCaptureLost += OnInkCanvasPointerCaptureLost;
}
+ private void ConfigureViewportGestures()
+ {
+ PanZoomInputLayer.PointerPressed += OnViewportPointerPressed;
+ PanZoomInputLayer.PointerMoved += OnViewportPointerMoved;
+ PanZoomInputLayer.PointerReleased += OnViewportPointerReleased;
+ PanZoomInputLayer.PointerCaptureLost += OnViewportPointerCaptureLost;
+ PanZoomInputLayer.PointerWheelChanged += OnViewportPointerWheelChanged;
+ PanZoomInputLayer.PointerTouchPadGestureMagnify += OnViewportTouchPadGestureMagnify;
+ PanZoomInputLayer.PointerTouchPadGestureSwipe += OnViewportTouchPadGestureSwipe;
+ }
+
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
@@ -148,7 +190,7 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
ComponentChromeCornerRadiusHelper.SafeValue(toolbarPaddingVertical, 4, 8));
ToolbarButtonsPanel.Spacing = toolbarSpacing;
- foreach (var button in new[] { PenButton, EraserButton, ClearButton, ExportButton })
+ foreach (var button in new[] { PenButton, EraserButton, HandButton, ClearButton, FileButton })
{
button.Width = buttonSize;
button.Height = buttonSize;
@@ -264,15 +306,21 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
return;
}
- _noteDirty = false;
_noteSaveTimer.Stop();
var noteSnapshot = BuildNoteSnapshot();
try
{
- _notePersistenceService.SaveNote(_componentId, _placementId, noteSnapshot, _noteRetentionDays);
+ if (_notePersistenceService.SaveNote(_componentId, _placementId, noteSnapshot, _noteRetentionDays))
+ {
+ _noteDirty = false;
+ }
}
- catch
+ catch (Exception ex)
{
+ AppLogger.Warn(
+ "Whiteboard",
+ $"Failed to force-save whiteboard note. ComponentId='{_componentId}'; PlacementId='{_placementId}'.",
+ ex);
}
}
@@ -287,8 +335,17 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
_noteSaveTimer.Stop();
_noteSaveTimer.Tick -= OnNoteSaveTimerTick;
InkCanvas.StrokeCollected -= OnInkCanvasStrokeCollected;
+ InkCanvas.StrokeErased -= OnInkCanvasStrokeErased;
InkCanvas.PointerReleased -= OnInkCanvasPointerReleased;
InkCanvas.PointerCaptureLost -= OnInkCanvasPointerCaptureLost;
+ PanZoomInputLayer.PointerPressed -= OnViewportPointerPressed;
+ PanZoomInputLayer.PointerMoved -= OnViewportPointerMoved;
+ PanZoomInputLayer.PointerReleased -= OnViewportPointerReleased;
+ PanZoomInputLayer.PointerCaptureLost -= OnViewportPointerCaptureLost;
+ PanZoomInputLayer.PointerWheelChanged -= OnViewportPointerWheelChanged;
+ PanZoomInputLayer.PointerTouchPadGestureMagnify -= OnViewportTouchPadGestureMagnify;
+ PanZoomInputLayer.PointerTouchPadGestureSwipe -= OnViewportTouchPadGestureSwipe;
+ ClearPanZoomPointers();
}
private void RecolorAllStrokes(SKColor targetColor)
@@ -298,7 +355,6 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
TrySetStrokeColor(InkCanvas.Strokes[i], targetColor);
}
- InkCanvas.AvaloniaSkiaInkCanvas.InvalidateBitmapCache();
InkCanvas.InvalidateVisual();
}
@@ -366,15 +422,24 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
private void SetToolMode(WhiteboardToolMode mode)
{
_toolMode = mode;
- InkCanvas.EditingMode = mode == WhiteboardToolMode.Pen
- ? InkCanvasEditingMode.Ink
- : InkCanvasEditingMode.EraseByPoint;
+ InkCanvas.EditingMode = mode switch
+ {
+ WhiteboardToolMode.Pen => InkCanvasEditingMode.Ink,
+ WhiteboardToolMode.Eraser => InkCanvasEditingMode.EraseByPoint,
+ _ => InkCanvasEditingMode.None
+ };
if (mode == WhiteboardToolMode.Pen)
{
InkCanvas.AvaloniaSkiaInkCanvas.Settings.InkColor = _selectedInkColor;
}
+ if (mode != WhiteboardToolMode.PanZoom)
+ {
+ ClearPanZoomPointers();
+ }
+
+ PanZoomInputLayer.IsHitTestVisible = mode == WhiteboardToolMode.PanZoom;
RefreshToolButtonVisuals();
}
@@ -407,8 +472,9 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
ApplyToolButtonVisual(PenButton, _toolMode == WhiteboardToolMode.Pen, activeBackground, activeForeground, idleBackground, idleForeground);
ApplyToolButtonVisual(EraserButton, _toolMode == WhiteboardToolMode.Eraser, activeBackground, activeForeground, idleBackground, idleForeground);
+ ApplyToolButtonVisual(HandButton, _toolMode == WhiteboardToolMode.PanZoom, activeBackground, activeForeground, idleBackground, idleForeground);
ApplyToolButtonVisual(ClearButton, false, activeBackground, activeForeground, idleBackground, idleForeground);
- ApplyToolButtonVisual(ExportButton, false, activeBackground, activeForeground, idleBackground, idleForeground);
+ ApplyToolButtonVisual(FileButton, false, activeBackground, activeForeground, idleBackground, idleForeground);
}
private static void ApplyToolButtonVisual(
@@ -476,12 +542,265 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
SetToolMode(WhiteboardToolMode.Eraser);
}
+ private void OnHandButtonClick(object? sender, RoutedEventArgs e)
+ {
+ SetToolMode(WhiteboardToolMode.PanZoom);
+ }
+
private void OnClearButtonClick(object? sender, RoutedEventArgs e)
{
ClearAllStrokes();
QueueNoteSave();
}
+ private void OnViewportPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (_toolMode != WhiteboardToolMode.PanZoom)
+ {
+ return;
+ }
+
+ if (e.Pointer.Type == PointerType.Mouse &&
+ !e.GetCurrentPoint(PanZoomInputLayer).Properties.IsLeftButtonPressed)
+ {
+ return;
+ }
+
+ _panZoomPointers[e.Pointer.Id] = e.GetPosition(PanZoomInputLayer);
+ TryCapturePointer(e.Pointer, PanZoomInputLayer);
+ ResetPanZoomGestureBaseline();
+ e.Handled = true;
+ }
+
+ private void OnViewportPointerMoved(object? sender, PointerEventArgs e)
+ {
+ if (_toolMode != WhiteboardToolMode.PanZoom ||
+ !_panZoomPointers.ContainsKey(e.Pointer.Id))
+ {
+ return;
+ }
+
+ if (e.Pointer.Type == PointerType.Mouse &&
+ !e.GetCurrentPoint(PanZoomInputLayer).Properties.IsLeftButtonPressed)
+ {
+ RemovePanZoomPointer(e.Pointer);
+ e.Handled = true;
+ return;
+ }
+
+ _panZoomPointers[e.Pointer.Id] = e.GetPosition(PanZoomInputLayer);
+ ApplyPanZoomPointerGesture();
+ e.Handled = true;
+ }
+
+ private void OnViewportPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ if (_toolMode != WhiteboardToolMode.PanZoom)
+ {
+ return;
+ }
+
+ RemovePanZoomPointer(e.Pointer);
+ e.Handled = true;
+ }
+
+ private void OnViewportPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
+ {
+ if (_toolMode != WhiteboardToolMode.PanZoom)
+ {
+ return;
+ }
+
+ _panZoomPointers.Remove(e.Pointer.Id);
+ ResetPanZoomGestureBaseline();
+ }
+
+ private void OnViewportPointerWheelChanged(object? sender, PointerWheelEventArgs e)
+ {
+ if (_toolMode != WhiteboardToolMode.PanZoom)
+ {
+ return;
+ }
+
+ var delta = e.Delta.Y;
+ if (!double.IsFinite(delta) || Math.Abs(delta) <= double.Epsilon)
+ {
+ return;
+ }
+
+ var zoomStep = Math.Clamp(delta, -4d, 4d);
+ var factor = Math.Pow(1.12d, zoomStep);
+ ZoomViewportAt(_viewportState.Zoom * factor, e.GetPosition(PanZoomInputLayer));
+ e.Handled = true;
+ }
+
+ private void OnViewportTouchPadGestureMagnify(object? sender, PointerDeltaEventArgs e)
+ {
+ if (_toolMode != WhiteboardToolMode.PanZoom)
+ {
+ return;
+ }
+
+ var delta = Math.Abs(e.Delta.Y) > double.Epsilon ? e.Delta.Y : e.Delta.X;
+ if (!double.IsFinite(delta) || Math.Abs(delta) <= double.Epsilon)
+ {
+ return;
+ }
+
+ var factor = Math.Clamp(1d + delta, 0.5d, 2d);
+ ZoomViewportAt(_viewportState.Zoom * factor, e.GetPosition(PanZoomInputLayer));
+ e.Handled = true;
+ }
+
+ private void OnViewportTouchPadGestureSwipe(object? sender, PointerDeltaEventArgs e)
+ {
+ if (_toolMode != WhiteboardToolMode.PanZoom)
+ {
+ return;
+ }
+
+ PanViewport(e.Delta);
+ e.Handled = true;
+ }
+
+ private void ApplyPanZoomPointerGesture()
+ {
+ if (_panZoomPointers.Count == 0)
+ {
+ _panZoomGestureBaseline = null;
+ return;
+ }
+
+ EnsureLogicalCanvasSize(expandToViewport: true);
+ if (_panZoomGestureBaseline is null)
+ {
+ ResetPanZoomGestureBaseline();
+ }
+
+ if (_panZoomGestureBaseline is not { } baseline)
+ {
+ return;
+ }
+
+ var viewportSize = GetViewportSize();
+ if (_panZoomPointers.Count == 1)
+ {
+ var currentPoint = _panZoomPointers.Values.First();
+ var delta = currentPoint - baseline.InitialCenter;
+ SetViewportState(
+ WhiteboardViewportHelper.PanBy(
+ baseline.StartViewport,
+ delta,
+ viewportSize,
+ _logicalCanvasSize),
+ queueSave: true);
+ return;
+ }
+
+ var points = _panZoomPointers.Values.Take(2).ToArray();
+ var currentCenter = GetCenter(points[0], points[1]);
+ var currentDistance = GetDistance(points[0], points[1]);
+ if (baseline.InitialDistance <= 1d || currentDistance <= 1d)
+ {
+ return;
+ }
+
+ var targetZoom = baseline.StartViewport.Zoom * (currentDistance / baseline.InitialDistance);
+ SetViewportState(
+ WhiteboardViewportHelper.ZoomFromGestureStart(
+ baseline.StartViewport,
+ targetZoom,
+ baseline.InitialCenter,
+ currentCenter,
+ viewportSize,
+ _logicalCanvasSize),
+ queueSave: true);
+ }
+
+ private void ResetPanZoomGestureBaseline()
+ {
+ if (_panZoomPointers.Count == 0)
+ {
+ _panZoomGestureBaseline = null;
+ return;
+ }
+
+ if (_panZoomPointers.Count == 1)
+ {
+ _panZoomGestureBaseline = new PanZoomGestureBaseline(
+ _viewportState,
+ _panZoomPointers.Values.First(),
+ 0d);
+ return;
+ }
+
+ var points = _panZoomPointers.Values.Take(2).ToArray();
+ _panZoomGestureBaseline = new PanZoomGestureBaseline(
+ _viewportState,
+ GetCenter(points[0], points[1]),
+ GetDistance(points[0], points[1]));
+ }
+
+ private void PanViewport(Vector delta)
+ {
+ EnsureLogicalCanvasSize(expandToViewport: true);
+ SetViewportState(
+ WhiteboardViewportHelper.PanBy(
+ _viewportState,
+ delta,
+ GetViewportSize(),
+ _logicalCanvasSize),
+ queueSave: true);
+ }
+
+ private void ZoomViewportAt(double targetZoom, Point anchorViewportPoint)
+ {
+ EnsureLogicalCanvasSize(expandToViewport: true);
+ SetViewportState(
+ WhiteboardViewportHelper.ZoomAt(
+ _viewportState,
+ targetZoom,
+ anchorViewportPoint,
+ GetViewportSize(),
+ _logicalCanvasSize),
+ queueSave: true);
+ }
+
+ private void RemovePanZoomPointer(IPointer pointer)
+ {
+ _panZoomPointers.Remove(pointer.Id);
+ TryCapturePointer(pointer, null);
+ ResetPanZoomGestureBaseline();
+ }
+
+ private void ClearPanZoomPointers()
+ {
+ _panZoomPointers.Clear();
+ _panZoomGestureBaseline = null;
+ }
+
+ private static Point GetCenter(Point first, Point second)
+ {
+ return new Point((first.X + second.X) * 0.5d, (first.Y + second.Y) * 0.5d);
+ }
+
+ private static double GetDistance(Point first, Point second)
+ {
+ return Math.Sqrt(Math.Pow(first.X - second.X, 2d) + Math.Pow(first.Y - second.Y, 2d));
+ }
+
+ private static void TryCapturePointer(IPointer pointer, IInputElement? target)
+ {
+ try
+ {
+ pointer.Capture(target);
+ }
+ catch
+ {
+ // Pointer capture is best-effort; the viewport still works without it.
+ }
+ }
+
private async void OnExportButtonClick(object? sender, RoutedEventArgs e)
{
var fileName = $"whiteboard-{DateTime.Now:yyyyMMdd-HHmmss}.svg";
@@ -524,10 +843,75 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
ExportSvgToStream(fileStream);
}
+ private async void OnImportButtonClick(object? sender, RoutedEventArgs e)
+ {
+ var topLevel = TopLevel.GetTopLevel(this);
+ var storageProvider = topLevel?.StorageProvider;
+ if (storageProvider is null)
+ {
+ return;
+ }
+
+ var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+ {
+ Title = "Open Whiteboard SVG",
+ AllowMultiple = false,
+ FileTypeFilter =
+ [
+ new FilePickerFileType("SVG image")
+ {
+ Patterns = ["*.svg"],
+ MimeTypes = ["image/svg+xml"]
+ }
+ ]
+ });
+
+ var importFile = files.FirstOrDefault();
+ if (importFile is null)
+ {
+ return;
+ }
+
+ try
+ {
+ await using var stream = await importFile.OpenReadAsync();
+ EnsureLogicalCanvasSize(expandToViewport: true);
+ var importResult = WhiteboardSvgImportService.Import(
+ stream,
+ Math.Max(1d, _logicalCanvasSize.Width),
+ Math.Max(1d, _logicalCanvasSize.Height));
+
+ if (importResult.Strokes.Count == 0)
+ {
+ AppLogger.Warn(
+ "Whiteboard",
+ $"SVG import did not contain supported paths. SkippedPathCount={importResult.SkippedPathCount}.");
+ return;
+ }
+
+ ClearAllStrokes();
+ ApplyNoteSnapshot(new WhiteboardNoteSnapshot
+ {
+ Version = 2,
+ CanvasWidth = Math.Max(1d, _logicalCanvasSize.Width),
+ CanvasHeight = Math.Max(1d, _logicalCanvasSize.Height),
+ BackgroundColor = ToHexColor((_isNightModeApplied ?? false) ? SKColors.Black : SKColors.White),
+ Strokes = importResult.Strokes
+ });
+ ResetViewportToFit(queueSave: false);
+ QueueNoteSave();
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("Whiteboard", "Failed to import SVG into whiteboard.", ex);
+ }
+ }
+
private void ExportSvgToStream(Stream stream)
{
- var width = Math.Max(1d, CanvasBorder.Bounds.Width);
- var height = Math.Max(1d, CanvasBorder.Bounds.Height);
+ EnsureLogicalCanvasSize(expandToViewport: true);
+ var width = Math.Max(1d, _logicalCanvasSize.Width);
+ var height = Math.Max(1d, _logicalCanvasSize.Height);
var bounds = SKRect.Create((float)width, (float)height);
using var svgCanvas = SKSvgCanvas.Create(bounds, stream);
@@ -560,6 +944,13 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
QueueNoteSave();
}
+ private void OnInkCanvasStrokeErased(object? sender, DotNetCampus.Inking.Contexts.ErasingCompletedEventArgs e)
+ {
+ _ = sender;
+ _ = e;
+ QueueNoteSave();
+ }
+
private void OnInkCanvasPointerReleased(object? sender, Avalonia.Input.PointerReleasedEventArgs e)
{
_ = sender;
@@ -593,9 +984,26 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
var componentId = _componentId;
var placementId = _placementId;
var retentionDays = _noteRetentionDays;
- _noteDirty = false;
+ var saveRevision = _noteSaveRevision;
_noteSaveTimer.Stop();
- _ = Task.Run(() => _notePersistenceService.SaveNote(componentId, placementId, noteSnapshot, retentionDays));
+ _ = Task.Run(() => _notePersistenceService.SaveNote(componentId, placementId, noteSnapshot, retentionDays))
+ .ContinueWith(task =>
+ {
+ var saved = task.Status == TaskStatus.RanToCompletion && task.Result;
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (_disposed || saveRevision != _noteSaveRevision)
+ {
+ return;
+ }
+
+ _noteDirty = !saved;
+ if (!saved && !_noteSaveTimer.IsEnabled)
+ {
+ _noteSaveTimer.Start();
+ }
+ });
+ });
}
private void QueueNoteSave()
@@ -606,10 +1014,9 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
}
_noteDirty = true;
- if (!_noteSaveTimer.IsEnabled)
- {
- _noteSaveTimer.Start();
- }
+ _noteSaveRevision++;
+ _noteSaveTimer.Stop();
+ _noteSaveTimer.Start();
}
private void PersistNoteImmediately()
@@ -624,15 +1031,21 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
return;
}
- _noteDirty = false;
_noteSaveTimer.Stop();
var noteSnapshot = BuildNoteSnapshot();
try
{
- _notePersistenceService.SaveNote(_componentId, _placementId, noteSnapshot, _noteRetentionDays);
+ if (_notePersistenceService.SaveNote(_componentId, _placementId, noteSnapshot, _noteRetentionDays))
+ {
+ _noteDirty = false;
+ }
}
- catch
+ catch (Exception ex)
{
+ AppLogger.Warn(
+ "Whiteboard",
+ $"Failed to persist whiteboard immediately. ComponentId='{_componentId}'; PlacementId='{_placementId}'.",
+ ex);
}
}
@@ -683,13 +1096,110 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
}
}
+ private void ApplySnapshotCanvasAndViewport(WhiteboardNoteSnapshot snapshot)
+ {
+ var viewportSize = GetViewportSize();
+ var canvasWidth = snapshot.CanvasWidth > 0 ? snapshot.CanvasWidth : viewportSize.Width;
+ var canvasHeight = snapshot.CanvasHeight > 0 ? snapshot.CanvasHeight : viewportSize.Height;
+ SetLogicalCanvasSize(new Size(canvasWidth, canvasHeight));
+ SetViewportState(
+ new WhiteboardViewportState(
+ snapshot.ViewportZoom,
+ new Vector(snapshot.ViewportOffsetX, snapshot.ViewportOffsetY)),
+ queueSave: false);
+ }
+
+ private void ResetViewportToFit(bool queueSave)
+ {
+ EnsureLogicalCanvasSize(expandToViewport: true);
+ SetViewportState(
+ WhiteboardViewportHelper.Fit(GetViewportSize(), _logicalCanvasSize),
+ queueSave);
+ }
+
+ private void EnsureLogicalCanvasSize(bool expandToViewport)
+ {
+ var viewportSize = GetViewportSize();
+ var normalizedCanvasSize = WhiteboardViewportHelper.NormalizeSize(_logicalCanvasSize);
+ if (_logicalCanvasSize.Width <= 1d || _logicalCanvasSize.Height <= 1d)
+ {
+ normalizedCanvasSize = viewportSize;
+ }
+ else if (expandToViewport)
+ {
+ normalizedCanvasSize = new Size(
+ Math.Max(normalizedCanvasSize.Width, viewportSize.Width),
+ Math.Max(normalizedCanvasSize.Height, viewportSize.Height));
+ }
+
+ SetLogicalCanvasSize(normalizedCanvasSize);
+ }
+
+ private void SetLogicalCanvasSize(Size canvasSize)
+ {
+ _logicalCanvasSize = WhiteboardViewportHelper.NormalizeSize(canvasSize);
+ InkCanvas.Width = _logicalCanvasSize.Width;
+ InkCanvas.Height = _logicalCanvasSize.Height;
+ }
+
+ private Size GetViewportSize()
+ {
+ var width = CanvasBorder.Bounds.Width > 1d
+ ? CanvasBorder.Bounds.Width
+ : Math.Max(1d, _currentCellSize * _baseWidthCells);
+ var height = CanvasBorder.Bounds.Height > 1d
+ ? CanvasBorder.Bounds.Height
+ : Math.Max(1d, Bounds.Height > 1d ? Bounds.Height : _currentCellSize * Math.Max(2, _baseWidthCells));
+
+ return WhiteboardViewportHelper.NormalizeSize(new Size(width, height));
+ }
+
+ private void SetViewportState(WhiteboardViewportState nextState, bool queueSave)
+ {
+ var next = WhiteboardViewportHelper.Clamp(nextState, GetViewportSize(), _logicalCanvasSize);
+ var changed = !AreViewportStatesClose(_viewportState, next);
+ _viewportState = next;
+ ApplyViewportTransform();
+
+ if (changed && queueSave)
+ {
+ QueueNoteSave();
+ }
+ }
+
+ private void ApplyViewportTransform()
+ {
+ SetLogicalCanvasSize(_logicalCanvasSize);
+ _viewportScaleTransform.ScaleX = _viewportState.Zoom;
+ _viewportScaleTransform.ScaleY = _viewportState.Zoom;
+ _viewportTranslateTransform.X = _viewportState.Offset.X;
+ _viewportTranslateTransform.Y = _viewportState.Offset.Y;
+ InkCanvas.InvalidateVisual();
+ }
+
+ private static bool AreViewportStatesClose(WhiteboardViewportState first, WhiteboardViewportState second)
+ {
+ const double tolerance = 0.001d;
+ return Math.Abs(first.Zoom - second.Zoom) <= tolerance &&
+ Math.Abs(first.Offset.X - second.Offset.X) <= tolerance &&
+ Math.Abs(first.Offset.Y - second.Offset.Y) <= tolerance;
+ }
+
private WhiteboardNoteSnapshot BuildNoteSnapshot()
{
+ EnsureLogicalCanvasSize(expandToViewport: true);
return new WhiteboardNoteSnapshot
{
+ Version = 2,
+ CanvasWidth = Math.Max(1d, _logicalCanvasSize.Width),
+ CanvasHeight = Math.Max(1d, _logicalCanvasSize.Height),
+ BackgroundColor = ToHexColor((_isNightModeApplied ?? false) ? SKColors.Black : SKColors.White),
+ ViewportZoom = _viewportState.Zoom,
+ ViewportOffsetX = _viewportState.Offset.X,
+ ViewportOffsetY = _viewportState.Offset.Y,
Strokes = InkCanvas.Strokes
.Select(BuildStrokeSnapshot)
- .Where(static stroke => stroke.Points.Count > 0)
+ .Where(static stroke => stroke.Points.Count > 0 || !string.IsNullOrWhiteSpace(stroke.PathSvgData))
.ToList()
};
}
@@ -702,6 +1212,7 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
Color = ToHexColor(stroke.Color),
InkThickness = stroke.InkThickness,
IgnorePressure = stroke.IgnorePressure,
+ PathSvgData = stroke.Path.ToSvgPathData(),
Points = pointList
.Select(static point => new WhiteboardStylusPointSnapshot
{
@@ -717,6 +1228,8 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
private void ApplyNoteSnapshot(WhiteboardNoteSnapshot snapshot)
{
+ ApplySnapshotCanvasAndViewport(snapshot);
+
if (snapshot.Strokes.Count == 0)
{
return;
@@ -728,24 +1241,29 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
var stylusPoints = strokeSnapshot.Points
.Select(ConvertStylusPoint)
.ToList();
- if (stylusPoints.Count == 0)
+ if (stylusPoints.Count == 0 && string.IsNullOrWhiteSpace(strokeSnapshot.PathSvgData))
{
continue;
}
- var path = renderer.RenderInkToPath(stylusPoints, strokeSnapshot.InkThickness);
+ var path = BuildRestoredStrokePath(stylusPoints, strokeSnapshot, renderer);
+ if (path.IsEmpty)
+ {
+ path.Dispose();
+ continue;
+ }
+
var staticStroke = SkiaStroke.CreateStaticStroke(
InkId.NewId(),
path,
new StylusPointListSpan(stylusPoints, 0, stylusPoints.Count),
ParseStrokeColor(strokeSnapshot.Color),
(float)strokeSnapshot.InkThickness,
- strokeSnapshot.IgnorePressure,
+ true,
renderer);
InkCanvas.AvaloniaSkiaInkCanvas.AddStaticStroke(staticStroke);
}
- InkCanvas.AvaloniaSkiaInkCanvas.UpdateBitmapCache();
InkCanvas.InvalidateVisual();
}
@@ -781,6 +1299,22 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
return $"#{color.Alpha:X2}{color.Red:X2}{color.Green:X2}{color.Blue:X2}";
}
+ private static SKPath BuildRestoredStrokePath(
+ IReadOnlyList stylusPoints,
+ WhiteboardStrokeSnapshot strokeSnapshot,
+ DotNetCampus.Inking.StrokeRenderers.ISkiaInkStrokeRenderer? renderer)
+ {
+ if (stylusPoints.Count > 0)
+ {
+ return renderer?.RenderInkToPath(stylusPoints, strokeSnapshot.InkThickness) ??
+ WhiteboardStrokePathBuilder.BuildPath(stylusPoints, strokeSnapshot.InkThickness);
+ }
+
+ return !string.IsNullOrWhiteSpace(strokeSnapshot.PathSvgData)
+ ? SKPath.ParseSvgPathData(strokeSnapshot.PathSvgData) ?? new SKPath()
+ : new SKPath();
+ }
+
private void ClearAllStrokes()
{
var strokeList = InkCanvas.Strokes.ToList();
@@ -799,8 +1333,6 @@ public partial class WhiteboardWidget : UserControl, IDesktopComponentWidget, IC
}
}
- InkCanvas.AvaloniaSkiaInkCanvas.UseBitmapCache(false);
- InkCanvas.AvaloniaSkiaInkCanvas.InvalidateBitmapCache();
InkCanvas.InvalidateVisual();
}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/DotNetCampus.AvaloniaInkCanvas.Avalonia12.csproj b/ThirdParty/DotNetCampus.InkCanvas/DotNetCampus.AvaloniaInkCanvas.Avalonia12.csproj
new file mode 100644
index 0000000..5a2cca4
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/DotNetCampus.AvaloniaInkCanvas.Avalonia12.csproj
@@ -0,0 +1,18 @@
+
+
+ net10.0
+ enable
+ enable
+ true
+ DotNetCampus.AvaloniaInkCanvas
+ DotNetCampus.Inking
+ false
+
+
+
+
+
+
+
+
+
diff --git a/ThirdParty/DotNetCampus.InkCanvas/README.md b/ThirdParty/DotNetCampus.InkCanvas/README.md
new file mode 100644
index 0000000..d5f5adc
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/README.md
@@ -0,0 +1,17 @@
+# DotNetCampus.InkCanvas Avalonia 12 Compatibility Fork
+
+This source is vendored from `dotnet-campus/DotNetCampus.InkCanvas` at commit
+`e4383cadc3ae206dd96f5b72ba889a007ebc44fa`, matching the
+`DotNetCampus.AvaloniaInkCanvas` 1.0.1 NuGet package previously used by the app.
+
+The local project keeps the assembly name `DotNetCampus.AvaloniaInkCanvas` so the
+host code can continue using the existing namespaces and APIs.
+
+Local compatibility changes:
+
+- Replace Avalonia 11 `Visual.VisualRoot` render scaling access with
+ `TopLevel.GetTopLevel(this)?.RenderScaling`.
+- Reference Avalonia 12 packages from a local project instead of the
+ Avalonia 11-targeted NuGet package.
+- Import the Avalonia 12 optional feature extension namespace for Skia custom
+ drawing operations.
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/API/InkCanvas.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/API/InkCanvas.cs
new file mode 100644
index 0000000..547153b
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/API/InkCanvas.cs
@@ -0,0 +1,282 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+using DotNetCampus.Inking.Contexts;
+using DotNetCampus.Inking.Erasing;
+using DotNetCampus.Inking.Interactives;
+using DotNetCampus.Inking.Primitive;
+using DotNetCampus.Inking.Utils;
+
+namespace DotNetCampus.Inking;
+
+public class InkCanvas : Control
+{
+ public InkCanvas()
+ {
+ var avaloniaSkiaInkCanvas = new AvaloniaSkiaInkCanvas()
+ {
+ IsHitTestVisible = false
+ };
+ AddChild(avaloniaSkiaInkCanvas);
+ AvaloniaSkiaInkCanvas = avaloniaSkiaInkCanvas;
+ HorizontalAlignment = HorizontalAlignment.Stretch;
+ VerticalAlignment = VerticalAlignment.Stretch;
+ }
+
+ public InkCanvasEditingMode EditingMode
+ {
+ get => _editingMode;
+ set
+ {
+ if (IsDuringInput)
+ {
+ throw new InvalidOperationException($"EditingMode should not be switched during the input process.");
+ }
+
+ _editingMode = value;
+ EditingModeChanged?.Invoke(this, EventArgs.Empty);
+ }
+ }
+
+ public event EventHandler? EditingModeChanged;
+
+ public IReadOnlyList Strokes => AvaloniaSkiaInkCanvas.StaticStrokeList;
+
+ private InkCanvasEditingMode _editingMode = InkCanvasEditingMode.Ink;
+
+ ///
+ /// 为 Avalonia 实现的基于 Skia 的 InkCanvas 笔迹画布
+ ///
+ public AvaloniaSkiaInkCanvas AvaloniaSkiaInkCanvas { get; }
+
+ private AvaloniaSkiaInkCanvasEraserMode EraserMode => AvaloniaSkiaInkCanvas.EraserMode;
+
+ ///
+ public event EventHandler? StrokeCollected
+ {
+ add => AvaloniaSkiaInkCanvas.StrokeCollected += value;
+ remove => AvaloniaSkiaInkCanvas.StrokeCollected -= value;
+ }
+
+ public event EventHandler? StrokeErased
+ {
+ add => EraserMode.ErasingCompleted += value;
+ remove => EraserMode.ErasingCompleted -= value;
+ }
+
+ #region Input
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ if (EditingMode == InkCanvasEditingMode.None)
+ {
+ return;
+ }
+
+ var args = ToArgs(e);
+ _inputDictionary[e.Pointer.Id] = new InputInfo(args.StylusPoint);
+
+ if (EditingMode == InkCanvasEditingMode.Ink)
+ {
+ if (!IsDuringInput)
+ {
+ AvaloniaSkiaInkCanvas.WritingStart();
+ }
+
+ AvaloniaSkiaInkCanvas.WritingDown(in args);
+ }
+ else if (EditingMode == InkCanvasEditingMode.EraseByPoint)
+ {
+ EraserMode.EraserDown(in args);
+ }
+
+ base.OnPointerPressed(e);
+ }
+
+ protected override void OnPointerMoved(PointerEventArgs e)
+ {
+ if (EditingMode == InkCanvasEditingMode.None)
+ {
+ return;
+ }
+
+ if (!_inputDictionary.TryGetValue(e.Pointer.Id, out var inputInfo))
+ {
+ // Mouse? Not pressed yet.
+ return;
+ }
+
+ var args = ToArgs(e, inputInfo);
+ inputInfo.LastStylusPoint = args.StylusPoint;
+
+ if (EditingMode == InkCanvasEditingMode.Ink)
+ {
+ AvaloniaSkiaInkCanvas.WritingMove(in args);
+ }
+ else if (EditingMode == InkCanvasEditingMode.EraseByPoint)
+ {
+ EraserMode.EraserMove(in args);
+ }
+
+ base.OnPointerMoved(e);
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ if (EditingMode == InkCanvasEditingMode.None)
+ {
+ return;
+ }
+
+ if (!_inputDictionary.Remove(e.Pointer.Id, out var inputInfo))
+ {
+ // Mouse? Not pressed yet.
+ return;
+ }
+
+ var args = ToArgs(e);
+ inputInfo.LastStylusPoint = args.StylusPoint;
+
+ if (EditingMode == InkCanvasEditingMode.Ink)
+ {
+ AvaloniaSkiaInkCanvas.WritingUp(in args);
+
+ if (!IsDuringInput)
+ {
+ AvaloniaSkiaInkCanvas.WritingCompleted();
+ }
+ }
+ else if (EditingMode == InkCanvasEditingMode.EraseByPoint)
+ {
+ EraserMode.EraserUp(in args);
+ }
+
+ base.OnPointerReleased(e);
+ }
+
+ class InputInfo
+ {
+ public InputInfo(InkStylusPoint lastStylusPoint)
+ {
+ LastStylusPoint = lastStylusPoint;
+ }
+
+ public InkStylusPoint LastStylusPoint { get; set; }
+ }
+
+ private readonly Dictionary _inputDictionary = [];
+ private bool IsDuringInput => _inputDictionary.Count != 0;
+
+ private InkingModeInputArgs ToArgs(PointerEventArgs args, InputInfo? inputInfo = null)
+ {
+ PointerPoint currentPoint = args.GetCurrentPoint(AvaloniaSkiaInkCanvas);
+ var inkStylusPoint = ToInkStylusPoint(currentPoint);
+
+ IReadOnlyList? stylusPointList = null;
+ var list = args.GetIntermediatePoints(AvaloniaSkiaInkCanvas);
+ if (list.Count > 1)
+ {
+ stylusPointList = list.Select(ToInkStylusPoint)
+ .ToList();
+ }
+
+ return new InkingModeInputArgs(args.Pointer.Id, inkStylusPoint, args.Timestamp)
+ {
+ StylusPointList = stylusPointList,
+ };
+
+ InkStylusPoint ToInkStylusPoint(PointerPoint point)
+ {
+ var pressure = EnsurePressure(currentPoint.Properties.Pressure);
+ var contactRect = currentPoint.Properties.ContactRect;
+ var width = contactRect.Width;
+ var height = contactRect.Height;
+
+ if (inputInfo is not null)
+ {
+ if (width == 0 && inputInfo.LastStylusPoint.Width is { } lastWidth)
+ {
+ width = lastWidth;
+ }
+
+ if (height == 0 && inputInfo.LastStylusPoint.Height is { } lastHeight)
+ {
+ height = lastHeight;
+ }
+ }
+
+ var stylusPoint = new InkStylusPoint(currentPoint.Position.ToPoint2D(), pressure)
+ {
+ Width = width != 0 ? width : null,
+ Height = height != 0 ? height : null,
+ };
+
+ return stylusPoint;
+ }
+
+ float EnsurePressure(float pressure)
+ {
+ // 这是一个修复补丁。在 Linux X11 上,如果前后两个点的压力是相同的,则后点将不会报告压力,此时 Avalonia 上将使用默认压力值 0.5 来填充压力值
+ // 为了避免压力值抖动,将压力值修正为上一个点的压力值
+ const float defaultPressure = InkStylusPoint.DefaultPressure;
+ if (inputInfo != null && (pressure == 0 || Math.Abs(pressure - defaultPressure) < 0.00001))
+ {
+ return inputInfo.LastStylusPoint.Pressure;
+ }
+
+ return pressure;
+ }
+ }
+
+ #endregion
+
+ internal void AddChild(Control childControl)
+ {
+ LogicalChildren.Add(childControl);
+ VisualChildren.Add(childControl);
+ }
+
+ internal void RemoveChild(Control childControl)
+ {
+ LogicalChildren.Remove(childControl);
+ VisualChildren.Remove(childControl);
+ }
+
+ protected override Size MeasureCore(Size availableSize)
+ {
+ var width = availableSize.Width;
+ var height = availableSize.Height;
+
+ if (double.IsInfinity(width))
+ {
+ width = 0;
+ }
+
+ if (double.IsInfinity(height))
+ {
+ height = 0;
+ }
+
+ base.MeasureCore(availableSize);
+ return new Size(width, height);
+ }
+
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var size = base.ArrangeOverride(finalSize);
+
+ return size;
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ // to enable hit testing
+ context.DrawRectangle(Brushes.Transparent, null, new Rect(new Point(), Bounds.Size));
+ }
+
+
+}
+
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/API/InkCanvasEditingMode.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/API/InkCanvasEditingMode.cs
new file mode 100644
index 0000000..2c04456
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/API/InkCanvasEditingMode.cs
@@ -0,0 +1,8 @@
+namespace DotNetCampus.Inking;
+
+public enum InkCanvasEditingMode
+{
+ None = 0,
+ Ink = 1,
+ EraseByPoint = 5,
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Caching/InkBitmapCache.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Caching/InkBitmapCache.cs
new file mode 100644
index 0000000..86db73f
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Caching/InkBitmapCache.cs
@@ -0,0 +1,464 @@
+using DotNetCampus.Inking.Contexts;
+using DotNetCampus.Logging;
+using DotNetCampus.Numerics;
+using DotNetCampus.Numerics.Geometry;
+
+using SkiaSharp;
+using System.Runtime.CompilerServices;
+using DotNetCampus.Inking.Utils;
+
+namespace DotNetCampus.Inking.Caching;
+
+///
+/// 辅助绘制笔迹的位图缓存。
+///
+internal sealed class InkBitmapCache : IDisposable
+{
+ private readonly object _lock = new();
+
+ private readonly AvaloniaSkiaInkCanvasSettings _settings;
+ private readonly int _uiThreadId;
+ private int _renderThreadId;
+ private bool _useCacheOnNextRender;
+ private InkBitmapCacheContext? _cacheContext;
+
+ ///
+ /// 笔迹缓存数据。
+ ///
+ ///
+ /// isValid 由 UI 线程写入,渲染线程读取。
+ /// CachedData 由渲染线程写入,渲染线程读取。
+ ///
+ private volatile ThreadSafeInkBitmapCachedData _threadSafeCachedData = new(false, null);
+
+ ///
+ /// 笔迹缓存上下文信息。
+ ///
+ ///
+ /// 此字段由 UI 线程写入,由渲染线程读取。
+ ///
+ private InkBitmapCacheContext? CacheContext
+ {
+ get => _cacheContext.VerifyOnRenderThread(_renderThreadId);
+ set => _cacheContext = value.VerifyOnUIThread(_uiThreadId);
+ }
+
+ ///
+ /// 将此值设置为 以在下次绘制时使用位图缓存;反之,设置为 将在下帧不使用位图缓存。
+ ///
+ ///
+ /// 如果希望立即使用位图缓存,请在将此值设置为 后立即调用 方法刷新渲染。
+ ///
+ public bool UseCacheOnNextRender
+ {
+ get => _useCacheOnNextRender;
+ set
+ {
+ if (Equals(_useCacheOnNextRender, value))
+ {
+ return;
+ }
+
+ _useCacheOnNextRender = value.VerifyOnUIThread(_uiThreadId);
+ Log.Info($"[Ink][InkBitmapCache] 将在下一帧{(value ? "开启" : "关闭")}笔迹位图缓存。");
+ InvalidateCache();
+ }
+ }
+
+ ///
+ /// 创建 的新实例。
+ ///
+ /// 包含位图缓存的设置。
+ public InkBitmapCache(AvaloniaSkiaInkCanvasSettings settings)
+ {
+ _uiThreadId = Environment.CurrentManagedThreadId;
+ _settings = settings;
+ }
+
+ ///
+ /// 更新缓存所需的上下文信息,包括 DPI(画板到屏幕的缩放)和元素变换(元素到画板的变换矩阵)。
+ ///
+ /// 画板相对于屏幕物理像素的缩放比例。
+ /// 画板可见区域的边界(画板坐标系)。
+ /// 笔迹变换到画板应使用的变换矩阵。
+ public void UpdateCacheContext(double dpi, BoundingBox2D visibleBounds, SimilarityTransformation2D transformFromInkToRoot)
+ {
+ this.VerifyOnUIThread(_uiThreadId);
+ CacheContext = (dpi, visibleBounds, transformFromInkToRoot);
+ InvalidateCache();
+ }
+
+ public void Dispose() => InvalidateCache();
+
+ ///
+ /// 使缓存失效,如果下次绘制时缓存启用( 为 ),则会重新生成缓存。
+ ///
+ ///
+ /// 注意,虽然使用此方法可以使缓存失效,以便下次绘制时会使用缓存;但如果不调用 方法更新上下文信息,则生成的新缓存依旧会使用旧的信息。
+ /// 在此情况下,生成的新缓存图片在参数(清晰度、旋转方向、缩放等)上会跟旧缓存图片完全相同,只是新增和擦除的笔迹会在新的缓存图片上体现出来。
+ /// 如果你希望按照新的笔迹旋转方向、缩放等生成匹配此时应有清晰度的缓存图片,你应该调用 而不是本方法。那个方法本质上也会使缓存失效的。
+ ///
+ internal void InvalidateCache()
+ {
+ this.VerifyOnUIThread(_uiThreadId);
+ lock (_lock)
+ {
+ var (_, data) = _threadSafeCachedData;
+ _threadSafeCachedData = new ThreadSafeInkBitmapCachedData(false, data);
+ }
+ }
+
+ ///
+ /// 以位图缓存的形式画出笔迹。
+ ///
+ /// 笔迹的路径。
+ /// 要绘制到的画布。
+ /// 用于绘制的画笔。
+ /// 在使用绘制的路径前,请先调用 方法。
+ internal void DrawBitmap(in ReadOnlySpan paths, SKCanvas canvas, SKPaint skPaint)
+ {
+ _renderThreadId = Environment.CurrentManagedThreadId;
+
+ if (paths.Length is 0 || paths.HasNoPoints())
+ {
+ // 没有任何路径,不需要绘制。
+ // 所有的路径都没有点,无法被绘制。
+ return;
+ }
+
+ var context = CacheContext ?? throw new InvalidOperationException("在使用绘制的路径前,请先调用 UpdateCacheContext 方法。");
+
+ var threadSafeCachedData = _threadSafeCachedData;
+ var (isValid, cachedData) = threadSafeCachedData;
+ if (!isValid || cachedData is not { } data)
+ {
+ cachedData?.Dispose();
+ // 如果缓存已失效,则重新生成缓存。
+ data = CreateBitmapData(paths, skPaint, context, _settings);
+ lock (_lock)
+ {
+ _threadSafeCachedData = new ThreadSafeInkBitmapCachedData(true, data);
+ }
+ }
+
+ DrawBitmapData(canvas, context, data);
+ }
+
+ ///
+ /// 创建笔迹的位图缓存。
+ ///
+ /// 笔迹的路径。
+ /// 用于绘制的画笔。
+ /// 缓存所用的上下文信息。
+ /// 位图缓存的设置。
+ /// 缓存的位图数据。
+ private static InkBitmapCachedData CreateBitmapData(in ReadOnlySpan paths, SKPaint skPaint, in InkBitmapCacheContext context, AvaloniaSkiaInkCanvasSettings settings)
+ {
+ // 将原始的笔迹数据(元素坐标系)转换为画板坐标系,并求取其边界。
+ var inkBounds = paths.GetTransformedBounds(context.TransformFromInkToRoot);
+ // 生成一个伪的可视区域,使其有画板区域的 41 倍大,以便生成全景笔迹图作为背景位图(将镂空前景位图)。放心最终不会有这么大的,一来会根据实际笔迹区域裁剪,二来会根据 settings.MaxBitmapCacheSize 限制位图大小。
+ var backVisibleBounds = context.VisibleBounds.Inflate(context.VisibleBounds.Width * 20, context.VisibleBounds.Height * 20);
+ // 生成一个真实的可视区域,使其在画板的周围扩大 100 个单位,以便生成高清前景位图。
+ var foreVisibleBounds = context.VisibleBounds.Inflate(100);
+
+ // 如果笔迹区域比可视区域小,那么全景笔迹图就是高清前景图。不再需要背景图了。
+ var isForeBitmapEnough = foreVisibleBounds.Contains(inkBounds);
+
+ // 创建低清背景位图,使其显示全景笔迹。
+ var backBitmap = isForeBitmapEnough
+ ? null
+ : CreateBitmapCore(paths, skPaint, context, settings, inkBounds, backVisibleBounds, foreVisibleBounds);
+
+ // 创建高清可见区域位图,使其高质量显示局部笔迹。
+ var foreBitmap = CreateBitmapCore(paths, skPaint, context, settings, inkBounds, foreVisibleBounds);
+
+ // 记录日志并返回数据。
+ var data = new InkBitmapCachedData(backBitmap, foreBitmap);
+ LogBitmapCached(data, inkBounds);
+ return data;
+ }
+
+ ///
+ /// 创建一张位图,使其显示笔迹的一部分。
+ ///
+ /// 要画的笔迹的路径。
+ /// 用于绘制的画笔。
+ /// 位图缓存所用的上下文信息。
+ /// 位图缓存的设置。
+ /// 所有笔迹的边界(画板坐标系)。
+ /// 画板的可视区域(画板坐标系)。
+ /// 如果需要镂空一个区域,则传入此区域的边界(画板坐标系)。
+ /// 创建的位图数据。
+ ///
+ /// 如果笔迹完全移出了可视区域,则不会创建位图,返回 。
+ ///
+ private static InkQualityBitmapData? CreateBitmapCore(
+ in ReadOnlySpan paths, SKPaint skPaint,
+ in InkBitmapCacheContext context, AvaloniaSkiaInkCanvasSettings settings,
+ BoundingBox2D inkBounds, BoundingBox2D visibleBounds, BoundingBox2D? clipBounds = null)
+ {
+ // 笔迹和可视区域的交集。使用此交集可以避免笔迹区域没那么大时生成过大的位图。
+ var intersectedBounds = inkBounds.Intersect(visibleBounds);
+ if (clipBounds is { } cb1)
+ {
+ intersectedBounds = intersectedBounds.Exclude(cb1);
+ }
+ if (intersectedBounds.IsEmpty)
+ {
+ // 如果交集为空,则不需要绘制。
+ return null;
+ }
+ // 根据用户的设置,决定要显示多清晰的一张位图缓存。
+ var (bitmapWidth, bitmapHeight, scalingRootToBitmap, quality) =
+ CalculateBestBitmapScaling(intersectedBounds.Width, intersectedBounds.Height, context.DpiScaling, settings.MaxBitmapCacheSize);
+
+ // 创建位图。
+ var bitmap = new SKBitmap(bitmapWidth, bitmapHeight, SKColorType.Bgra8888, SKAlphaType.Premul);
+ using var canvas = new SKCanvas(bitmap);
+
+ // 将位图坐标系转换为笔迹坐标系,这样后面画笔迹时可以直接使用其数据。
+ canvas.Scale((float) scalingRootToBitmap, (float) scalingRootToBitmap);
+ canvas.Translate(-(float) intersectedBounds.MinX, -(float) intersectedBounds.MinY);
+ if (clipBounds is { } cb2)
+ {
+ // 如果有镂空需求,则镂空一个区域(画板坐标系)。
+ // 当我们期望用一个全景背景图和高清前景图拼接时,背景图就需要将前景图的区域镂空。
+ canvas.ClipRect(cb2, SKClipOperation.Difference);
+ }
+ var skMatrix = context.TransformFromInkToRoot.ToSkMatrix();
+ canvas.Concat(ref skMatrix);
+
+ // 使用笔迹坐标系绘制笔迹。
+ foreach (var c in paths)
+ {
+ skPaint.Color = c.Color;
+ canvas.DrawPath(c.Path, skPaint);
+ }
+
+ // 返回位图数据。
+ return new(bitmap, intersectedBounds, scalingRootToBitmap, quality);
+ }
+
+ ///
+ /// 画出参数 中指定的多张缓存位图。
+ ///
+ /// 要绘制到的画布。
+ /// 缓存所用的上下文信息。
+ /// 缓存的位图数据。
+ private static void DrawBitmapData(SKCanvas canvas, in InkBitmapCacheContext context, in InkBitmapCachedData data)
+ {
+ // 绘制镂空的笔迹全景图。
+ if (data.BackBitmap is { } backBitmap)
+ {
+ DrawBitmapCore(canvas, in context, in backBitmap);
+ }
+
+ // 绘制高清的笔迹局部图。
+ if (data.ForeBitmap is { } foreBitmap)
+ {
+ DrawBitmapCore(canvas, in context, in foreBitmap);
+ }
+ }
+
+ ///
+ /// 画出参数 中指定的单张缓存位图。
+ ///
+ /// 要绘制到的画布。
+ /// 缓存所用的上下文信息。
+ /// 缓存的位图数据。
+ private static void DrawBitmapCore(SKCanvas canvas, in InkBitmapCacheContext context, in InkQualityBitmapData data)
+ {
+ var numericMatrix = canvas.TotalMatrix.ToSimilarityTransformation();
+ var transform = SimilarityTransformation2D.Identity
+ .Scale(1 / data.ScalingRootToBitmap)
+ .Translate(new Vector2D(data.InkBounds.MinX, data.InkBounds.MinY))
+ .Apply(context.TransformFromInkToRoot.Inverse())
+ .Apply(numericMatrix);
+
+ canvas.Save();
+ canvas.SetMatrix(transform.ToSkMatrix());
+ try
+ {
+ using var paint = new SKPaint();
+ paint.IsAntialias = true;
+ paint.FilterQuality = SKFilterQuality.High;
+ canvas.DrawBitmap(data.Bitmap, 0, 0, paint);
+ }
+ finally
+ {
+ canvas.Restore();
+ }
+ }
+
+ ///
+ /// 计算最佳的位图缩放比例,以便在显示时不会产生过大的位图。
+ ///
+ /// 笔迹在笔迹元素坐标系中的宽度。
+ /// 笔迹在笔迹元素坐标系中的高度。
+ /// 画板到屏幕像素的缩放比例。
+ /// 缓存位图的最大像素数(避免过大导致性能问题)。
+ ///
+ /// 在最佳缩放比例下的:
+ /// BitmapWidth:位图的宽度(像素)。
+ /// BitmapHeight:位图的高度(像素)。
+ /// Quality:位图的清晰度,即位图的像素数与最大像素数的比例的平方根。
+ /// ScalingInkToBitmap:笔迹/元素坐标系内的长度乘以此值,可以得到位图缓存中的像素长度。
+ ///
+ private static (int BitmapWidth, int BitmapHeight, double ScalingRootToBitmap, double Quality) CalculateBestBitmapScaling(
+ double inkWidth, double inkHeight, double dpiScaling, int maxBitmapPixelCount)
+ {
+ // 为防止计算溢出(当宽高足够大时),下面的像素数计算我们尽量使用 double 类型。
+ var desiredBitmapPixelWidth = (int) Math.Round(inkWidth * dpiScaling);
+ var desiredBitmapPixelHeight = (int) Math.Round(inkHeight * dpiScaling);
+ var totalBitmapPixelCount = desiredBitmapPixelWidth * (double) desiredBitmapPixelHeight;
+
+ // 如果原尺寸显示笔迹也不会产生太大的位图,则直接使用原尺寸。
+ if (totalBitmapPixelCount <= maxBitmapPixelCount)
+ {
+ return (desiredBitmapPixelWidth, desiredBitmapPixelHeight, dpiScaling, 1d);
+ }
+
+ // 如果原尺寸显示笔迹会产生过大的位图,则缩小位图,以更低清晰度来显示。
+ var quality = Math.Sqrt(maxBitmapPixelCount / totalBitmapPixelCount);
+ return (
+ (int) Math.Round(desiredBitmapPixelWidth * quality),
+ (int) Math.Round(desiredBitmapPixelHeight * quality),
+ dpiScaling * quality,
+ quality);
+ }
+
+ ///
+ /// 记录一条日志,表示笔迹位图缓存已更新。
+ ///
+ ///
+ ///
+ private static void LogBitmapCached(InkBitmapCachedData data, BoundingBox2D inkBounds) => Log.Info(data switch
+ {
+ ({ } bb, { } fb) =>
+ $"[Ink][InkBitmapCache] 笔迹位图缓存更新。笔迹 {inkBounds.Width}×{inkBounds.Height},全景图 {bb.Bitmap.Width}×{bb.Bitmap.Height}(清晰度 {bb.Quality}),前景图 {fb.Bitmap.Width}×{fb.Bitmap.Height}(清晰度 {fb.Quality})",
+ ({ } bb, null) =>
+ $"[Ink][InkBitmapCache] 笔迹位图缓存更新。笔迹 {inkBounds.Width}×{inkBounds.Height},全景图 {bb.Bitmap.Width}×{bb.Bitmap.Height}(清晰度 {bb.Quality})",
+ (null, { } fb) =>
+ $"[Ink][InkBitmapCache] 笔迹位图缓存更新。笔迹 {inkBounds.Width}×{inkBounds.Height},前景图 {fb.Bitmap.Width}×{fb.Bitmap.Height}(清晰度 {fb.Quality})",
+ _ =>
+ $"[Ink][InkBitmapCache] 笔迹位图缓存无需更新。笔迹 {inkBounds.Width}×{inkBounds.Height},不在可视区域内。",
+ });
+
+ private record ThreadSafeInkBitmapCachedData(bool IsValid, InkBitmapCachedData? CachedData);
+}
+
+///
+/// 包含一些用于 的扩展方法。
+///
+file static class Extensions
+{
+ internal static T VerifyOnUIThread(this T field, int threadId, [CallerMemberName] string callerMemberName = "")
+ {
+ if (Environment.CurrentManagedThreadId != threadId)
+ {
+ throw new InvalidOperationException($"成员 {callerMemberName} 只能在 UI 线程访问。");
+ }
+ return field;
+ }
+
+ internal static T VerifyOnRenderThread(this T field, int threadId, [CallerMemberName] string callerMemberName = "")
+ {
+ if (Environment.CurrentManagedThreadId != threadId)
+ {
+ throw new InvalidOperationException($"成员 {callerMemberName} 只能在渲染线程访问。");
+ }
+ return field;
+ }
+
+ internal static BoundingBox2D GetTransformedBounds(this in ReadOnlySpan paths, SimilarityTransformation2D transform)
+ {
+ var bounds = paths[0].Path.GetTransformedBounds(transform);
+ for (var i = 1; i < paths.Length; i++)
+ {
+ var path = paths[i].Path;
+ if (path.Points.Length > 0)
+ {
+ bounds.Union(path.GetTransformedBounds(transform));
+ }
+ }
+ return BoundingBox2D.Create(bounds.Left, bounds.Top, bounds.Right, bounds.Bottom);
+ }
+
+ private static SKRect GetTransformedBounds(this SKPath path, SimilarityTransformation2D transform)
+ {
+ using var newPath = new SKPath(path);
+ newPath.Transform(transform.ToSkMatrix());
+ return newPath.Bounds;
+ }
+
+ internal static void ClipRect(this SKCanvas canvas, BoundingBox2D clipBounds, SKClipOperation operation = SKClipOperation.Intersect)
+ {
+ canvas.ClipRect(new SKRect(
+ (float) clipBounds.MinX,
+ (float) clipBounds.MinY,
+ (float) clipBounds.MaxX,
+ (float) clipBounds.MaxY
+ ), operation);
+ }
+
+ ///
+ /// 已知一个矩形边界,排除另一个矩形边界;返回排除后异形图形的边界。
+ ///
+ /// 原始边界。
+ /// 要排除掉的边界。
+ /// 排除后的边界。
+ internal static BoundingBox2D Exclude(this BoundingBox2D bounds, BoundingBox2D excludeBounds)
+ {
+ var includeTopLeft = excludeBounds.Contains(new Point2D(bounds.MinX, bounds.MinY));
+ var includeTopRight = excludeBounds.Contains(new Point2D(bounds.MaxX, bounds.MinY));
+ var includeBottomRight = excludeBounds.Contains(new Point2D(bounds.MaxX, bounds.MaxY));
+ var includeBottomLeft = excludeBounds.Contains(new Point2D(bounds.MinX, bounds.MaxY));
+
+ if (includeTopLeft && includeTopRight && includeBottomRight && includeBottomLeft)
+ {
+ return BoundingBox2D.Empty;
+ }
+
+ if (includeTopLeft && includeBottomLeft)
+ {
+ return BoundingBox2D.Create(excludeBounds.MaxX, bounds.MinY, bounds.MaxX, bounds.MaxY);
+ }
+
+ if (includeTopLeft && includeTopRight)
+ {
+ return BoundingBox2D.Create(bounds.MinX, excludeBounds.MaxY, bounds.MaxX, bounds.MaxY);
+ }
+
+ if (includeTopRight && includeBottomRight)
+ {
+ return BoundingBox2D.Create(bounds.MinX, bounds.MinY, excludeBounds.MinX, bounds.MaxY);
+ }
+
+ if (includeBottomRight && includeBottomLeft)
+ {
+ return BoundingBox2D.Create(bounds.MinX, bounds.MinY, bounds.MaxX, excludeBounds.MinY);
+ }
+
+ return bounds;
+ }
+
+ ///
+ /// 判断所有路径是否都没有点。(这种路径没有宽高,无法被绘制。)
+ ///
+ /// 要检查的路径。
+ /// 如果所有路径都没有点,则返回 ;否则返回 。
+ public static bool HasNoPoints(this in ReadOnlySpan paths)
+ {
+ var allZeroPoints = true;
+ for (var i = 0; i < paths.Length; i++)
+ {
+ var path = paths[i];
+ if (path.Path.Points.Length is not 0)
+ {
+ allZeroPoints = false;
+ break;
+ }
+ }
+ return allZeroPoints;
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Caching/InkBitmapCacheContext.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Caching/InkBitmapCacheContext.cs
new file mode 100644
index 0000000..58069f0
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Caching/InkBitmapCacheContext.cs
@@ -0,0 +1,46 @@
+using DotNetCampus.Numerics.Geometry;
+
+using SkiaSharp;
+
+namespace DotNetCampus.Inking.Caching;
+
+///
+/// 为 的缓存提供上下文信息。
+///
+/// 画板到屏幕像素的缩放量。
+/// 画板可见区域的边界
+/// 笔迹变换到画板(要求没有额外变换)的变换矩阵。
+internal readonly record struct InkBitmapCacheContext(double DpiScaling, BoundingBox2D VisibleBounds, SimilarityTransformation2D TransformFromInkToRoot)
+{
+ public static implicit operator InkBitmapCacheContext((double ScalingFromRootToDevice, BoundingBox2D VisibleBounds, SimilarityTransformation2D TransformFromInkToRoot) tuple)
+ => new(tuple.ScalingFromRootToDevice, tuple.VisibleBounds, tuple.TransformFromInkToRoot);
+}
+
+///
+/// 为 的缓存提供数据。
+///
+/// 低清背景位图,显示全景笔迹。(如果笔迹本身不大,则此全景笔迹位图是 。)
+/// 高清可见区域位图,高质量显示局部笔迹。(如果笔迹本身不大,则此高清位图很有可能就是全景笔迹图。如果笔迹完全移出了可视区域,则此高清位图是 。)
+internal readonly record struct InkBitmapCachedData(InkQualityBitmapData? BackBitmap, InkQualityBitmapData? ForeBitmap) : IDisposable
+{
+ public void Dispose()
+ {
+ BackBitmap?.Dispose();
+ ForeBitmap?.Dispose();
+ }
+}
+
+///
+/// 为 的缓存提供数据。
+///
+/// 位图。
+/// 本次缓存的位图所使用的笔迹边界。
+/// 画板到位图的缩放量。
+/// 位图的清晰度,即位图的像素数与最大像素数的比例的平方根。
+internal readonly record struct InkQualityBitmapData(SKBitmap Bitmap, BoundingBox2D InkBounds, double ScalingRootToBitmap, double Quality) : IDisposable
+{
+ public void Dispose()
+ {
+ Bitmap.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/AvaloniaSkiaInkCanvasContext.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/AvaloniaSkiaInkCanvasContext.cs
new file mode 100644
index 0000000..8c525b1
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/AvaloniaSkiaInkCanvasContext.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DotNetCampus.Inking.Contexts;
+
+internal class AvaloniaSkiaInkCanvasContext
+{
+ private bool _isUsingBitmapCache;
+
+ ///
+ /// 获取可以由业务指定的笔迹设置。
+ ///
+ public AvaloniaSkiaInkCanvasSettings Settings { get; } = new();
+
+ ///
+ /// 如果指定为 ,则立即使用位图缓存替代真实的笔迹。
+ /// 否则,位图缓存将不会工作,而使用真实的笔迹渲染。
+ ///
+ ///
+ /// 在合适的时机切换真实的笔迹和位图缓存,可能可以提高性能。
+ /// 例如,在书写时,使用真实的笔迹可以提高书写性能;在漫游时,使用位图缓存可以提高漫游性能。
+ ///
+ public bool ShouldUseBitmapCache => Settings.IsBitmapCacheEnabled && _isUsingBitmapCache;
+
+ ///
+ /// 指定是否立即使用位图缓存替代真实的笔迹,或是使用真实的笔迹渲染。
+ ///
+ ///
+ /// 指定为 ,则立即使用位图缓存替代真实的笔迹;
+ /// 指定为 ,则位图缓存将不会工作,而使用真实的笔迹渲染。
+ ///
+ ///
+ /// 注意,使用此方法和修改用户设置方法 的本质不同为:
+ ///
+ /// - 此方法决定在不同的程序状态下是否应使用位图缓存,例如书写时不应开启缓存,而漫游时应该开启缓存;
+ /// - 而修改用户设置则是一个开关选项,仅决定在上述合适的状态下是否应使用位图缓存。当遇到不合适的程序状态时位图缓存依然不会生效。
+ ///
+ /// 所以,正确的做法是:
+ ///
+ /// - 应用开发者在应用程序初始化时设置用户设置 ;
+ /// - 框架开发者在实现位图缓存时,在适当的时机开启和关闭位图缓存,例如书写时关闭,漫游时开启。
+ ///
+ ///
+ public void UseBitmapCache(bool useBitmapCache)
+ {
+ _isUsingBitmapCache = useBitmapCache;
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/AvaloniaSkiaInkCanvasSettings.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/AvaloniaSkiaInkCanvasSettings.cs
new file mode 100644
index 0000000..3e40525
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/AvaloniaSkiaInkCanvasSettings.cs
@@ -0,0 +1,110 @@
+using Avalonia;
+using DotNetCampus.Inking.Erasing;
+using DotNetCampus.Inking.StrokeRenderers;
+using SkiaSharp;
+
+namespace DotNetCampus.Inking.Contexts;
+
+///
+/// 笔迹画布设置
+///
+public class AvaloniaSkiaInkCanvasSettings
+{
+ ///
+ /// 笔迹粗细。
+ ///
+ public float InkThickness { get; set; } = DefaultInkThickness;
+
+ ///
+ /// 默认的笔迹粗细
+ ///
+ public static float DefaultInkThickness => 10;
+
+ ///
+ /// 笔迹颜色。
+ ///
+ public SKColor InkColor { get; set; } = DefaultInkColor;
+
+ public static SKColor DefaultInkColor => SKColors.Red;
+
+ ///
+ /// 橡皮擦尺寸,可以在业务层,在手势橡皮擦过程中更改
+ ///
+ public Size EraserSize { get; set; } = DefaultEraserSize;
+
+ ///
+ /// 默认的橡皮擦尺寸
+ ///
+ public static Size DefaultEraserSize => new Size(48,
+ 72);
+
+ ///
+ /// 橡皮擦界面的创建器,默认为空使用默认的橡皮擦界面创建器
+ ///
+ public IEraserViewCreator? EraserViewCreator { get; set; }
+
+ ///
+ /// 将触摸尺寸当成橡皮擦尺寸,即橡皮擦大小不完全跟随 尺寸,而是会根据 的触摸大小决定
+ ///
+ public bool EnableStylusSizeAsEraserSize { get; set; } = true;
+
+ ///
+ /// 橡皮擦是否可以一直按照触摸尺寸修改橡皮擦尺寸。属于演示效果较好,实际使用效果差。仅当 为 true 时此属性才有效。为 false 时,将在超过 时间,设置为最后的触摸面积固定大小,即只允许在开始擦的时候根据触摸面积修改大小,之后将固定大小
+ ///
+ public bool CanEraserAlwaysFollowsTouchSize { set; get; } = false;
+
+ ///
+ /// 是否允许使用位图缓存在合适的时候替代真实的笔迹以提升部分场景下的笔迹性能。
+ ///
+ ///
+ /// 如果指定为 ,则书写模块会在合适的时机切换真实的笔迹和位图缓存,可能可以提高性能。
+ /// 如果指定为 ,则位图缓存将不会工作,将一直使用真实的笔迹渲染。
+ ///
+ public bool IsBitmapCacheEnabled { get; set; } = true;
+
+ ///
+ /// 橡皮擦可以根据触摸面积尺寸修改橡皮擦大小的时间。如果 为 true 则此属性无效。仅当 为 true 时此属性才有效
+ ///
+ public TimeSpan EraserCanResizeDuringTimeSpan { set; get; } = TimeSpan.FromMilliseconds(600);
+
+ ///
+ /// 是否锁定最小橡皮擦尺寸,即要求橡皮擦尺寸最小为 大小
+ ///
+ public bool LockMinEraserSize { init; get; } = true;
+
+ ///
+ /// 最小橡皮擦尺寸。仅当 为 true 时生效
+ ///
+ public Size MinEraserSize { init; get; } = DefaultEraserSize;
+
+ ///
+ /// 最大橡皮擦尺寸。理论上用不着,只是用来限制尺寸而已
+ ///
+ public Size MaxEraserSize { init; get; } = new Size(600, 600);
+
+ ///
+ /// 当使用位图缓存( 为 )时,最大的位图缓存大小。单位为像素。
+ ///
+ public int MaxBitmapCacheSize { get; set; } =
+ // 兆芯上似乎 1920×1080 都扛不住???
+ OperatingSystem.IsLinux() ? 1080 * 1080 :
+ // 主流 Intel/AMD 上目前看性能还行。
+ OperatingSystem.IsWindows() ? 2560 * 1440 :
+ // 默认随便给个值吧。
+ 1920 * 1080;
+
+ ///
+ /// 是否需要重新创建笔迹点,采用平滑滤波算法
+ ///
+ public bool ShouldReCreatePoint { get; set; }
+
+ ///
+ /// 笔迹渲染器。为空将使用默认的笔迹渲染器
+ ///
+ public ISkiaInkStrokeRenderer? InkStrokeRenderer { get; set; }
+
+ ///
+ /// 设置或获取是否需要忽略压感
+ ///
+ public bool IgnorePressure { get; set; }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/AvaloniaSkiaInkCanvasStrokeCollectedEventArgs.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/AvaloniaSkiaInkCanvasStrokeCollectedEventArgs.cs
new file mode 100644
index 0000000..46e764d
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/AvaloniaSkiaInkCanvasStrokeCollectedEventArgs.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DotNetCampus.Inking.Contexts;
+
+public class AvaloniaSkiaInkCanvasStrokeCollectedEventArgs : EventArgs
+{
+ public AvaloniaSkiaInkCanvasStrokeCollectedEventArgs(int stylusDeviceId, SkiaStroke skiaStroke)
+ {
+ StylusDeviceId = stylusDeviceId;
+ SkiaStroke = skiaStroke;
+ }
+
+ public int StylusDeviceId { get; }
+ public SkiaStroke SkiaStroke { get; }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/DynamicStrokeContext.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/DynamicStrokeContext.cs
new file mode 100644
index 0000000..e243d24
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/DynamicStrokeContext.cs
@@ -0,0 +1,39 @@
+using DotNetCampus.Inking.Interactives;
+
+namespace DotNetCampus.Inking.Contexts;
+
+///
+/// 动态笔迹层的上下文,一个手指落下一个对象
+///
+class DynamicStrokeContext
+{
+ public DynamicStrokeContext(InkingModeInputArgs lastInputArgs, AvaloniaSkiaInkCanvas canvas)
+ {
+ LastInputArgs = lastInputArgs;
+
+ var settings = canvas.Context.Settings;
+
+ SkiaSimpleInkRender? simpleInkRender = null;
+
+ if(settings.InkStrokeRenderer is null)
+ {
+ simpleInkRender = canvas.SimpleInkRender;
+ }
+
+ Stroke = new SkiaStroke(InkId.NewId())
+ {
+ Color = settings.InkColor,
+ InkThickness = settings.InkThickness,
+ IgnorePressure = settings.IgnorePressure,
+ InkStrokeRenderer = settings.InkStrokeRenderer,
+ SimpleInkRender = simpleInkRender,
+ };
+ }
+
+ public InkingModeInputArgs LastInputArgs { get; }
+
+ public int Id => LastInputArgs.Id;
+
+ public SkiaStroke Stroke { get; }
+ public override string ToString() => $"DynamicStrokeContext_{Id}";
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/ErasingCompletedEventArgs.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/ErasingCompletedEventArgs.cs
new file mode 100644
index 0000000..b4a6a04
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/ErasingCompletedEventArgs.cs
@@ -0,0 +1,13 @@
+using DotNetCampus.Inking.Erasing;
+
+namespace DotNetCampus.Inking.Contexts;
+
+public class ErasingCompletedEventArgs : EventArgs
+{
+ public ErasingCompletedEventArgs(IReadOnlyList erasingSkiaStrokeList)
+ {
+ ErasingSkiaStrokeList = erasingSkiaStrokeList;
+ }
+
+ public IReadOnlyList ErasingSkiaStrokeList { get; }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/SkiaStrokeDrawContext.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/SkiaStrokeDrawContext.cs
new file mode 100644
index 0000000..81175e6
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Contexts/SkiaStrokeDrawContext.cs
@@ -0,0 +1,37 @@
+using Avalonia;
+using SkiaSharp;
+
+namespace DotNetCampus.Inking.Contexts;
+
+readonly record struct SkiaStrokeDrawContext(SKColor Color, SKPath Path, Rect DrawBounds, SKMatrix Transform, bool ShouldDisposePath) : IDisposable
+{
+ public void Dispose()
+ {
+ if (ShouldDisposePath)
+ {
+ Path.Dispose();
+ }
+ }
+}
+
+static class SkiaStrokeDrawContextExtension
+{
+ public static void DrawStroke(this SKCanvas canvas, in SkiaStrokeDrawContext skiaStrokeDrawContext, SKPaint skPaint)
+ {
+ skPaint.Color = skiaStrokeDrawContext.Color;
+ var transform = skiaStrokeDrawContext.Transform;
+ var useTransform = transform != SKMatrix.Empty && transform != SKMatrix.Identity;
+ if (useTransform)
+ {
+ canvas.Save();
+ canvas.Concat(ref transform);
+ }
+
+ canvas.DrawPath(skiaStrokeDrawContext.Path, skPaint);
+
+ if (useTransform)
+ {
+ canvas.Restore();
+ }
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Core/AvaloniaSkiaInkCanvas.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Core/AvaloniaSkiaInkCanvas.cs
new file mode 100644
index 0000000..5f73007
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Core/AvaloniaSkiaInkCanvas.cs
@@ -0,0 +1,618 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering.Composition;
+using Avalonia.Rendering.Composition.Transport;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Skia;
+
+using DotNetCampus.Inking.Caching;
+using DotNetCampus.Inking.Contexts;
+using DotNetCampus.Inking.Erasing;
+using DotNetCampus.Logging;
+using DotNetCampus.Numerics.Geometry;
+
+using SkiaSharp;
+
+using System.Diagnostics;
+using DotNetCampus.Inking.Interactives;
+using DotNetCampus.Inking.Utils;
+using InkTransformContext = (DotNetCampus.Numerics.Geometry.BoundingBox2D VisibleBounds, DotNetCampus.Numerics.Geometry.SimilarityTransformation2D TransformToRoot);
+
+namespace DotNetCampus.Inking;
+
+///
+/// 为 Avalonia 实现的基于 Skia 的 InkCanvas 笔迹画布,可提供动态和静态笔迹层
+///
+/// 既可以单独用作笔迹绘制的接收输入层执行绘制,也可以作为静态笔迹层的承载
+public class AvaloniaSkiaInkCanvas : Control
+{
+ private readonly InkBitmapCache _cache;
+ private InkTransformContext? _inkTransformContext;
+
+ public AvaloniaSkiaInkCanvas()
+ {
+ _cache = new InkBitmapCache(Context.Settings);
+
+ // 以下是调试代码,用于从文件中读取点列表,绘制到画布上
+ // 测试文件要求: 一行一个点,使用逗号分隔,格式为 x,y
+ //#if DEBUG
+ // var inkPointList = Path.Join(AppContext.BaseDirectory, "Assets", "Tests", "InkPointList.txt");
+ // if (File.Exists(inkPointList))
+ // {
+ // List<(double x, double y)> pointList = [];
+ // var lines = File.ReadAllLines(inkPointList);
+ // foreach (var line in lines)
+ // {
+ // var point = line.Split(',');
+ // var x = double.Parse(point[0]);
+ // var y = double.Parse(point[1]);
+ // pointList.Add((x, y));
+ // }
+
+ // var skiaStroke = new SkiaStroke(InkId.NewId())
+ // {
+ // Color = SKColors.Red,
+ // InkThickness = 20,
+ // InkCanvas = this,
+ // };
+ // skiaStroke.AddPoints(pointList.Select(t => new StylusPoint(t.x, t.y)));
+ // skiaStroke.SetAsStatic();
+ // AddStaticStroke(skiaStroke);
+ // }
+ //#endif
+ }
+
+ ///
+ /// 获取笔迹渲染相关的设置和状态上下文。
+ ///
+ internal AvaloniaSkiaInkCanvasContext Context { get; } = new();
+
+ ///
+ /// 获取可以由业务指定的笔迹设置。
+ ///
+ public AvaloniaSkiaInkCanvasSettings Settings => Context.Settings;
+
+ ///
+ /// 共享的简单笔迹渲染器
+ ///
+ internal SkiaSimpleInkRender SimpleInkRender => _skiaSimpleInkRender ??= new SkiaSimpleInkRender();
+ private SkiaSimpleInkRender? _skiaSimpleInkRender;
+
+ internal void AddChild(Control childControl)
+ {
+ LogicalChildren.Add(childControl);
+ VisualChildren.Add(childControl);
+ }
+
+ internal void RemoveChild(Control childControl)
+ {
+ LogicalChildren.Remove(childControl);
+ VisualChildren.Remove(childControl);
+ }
+
+ ///
+ /// 橡皮擦模式
+ ///
+ public AvaloniaSkiaInkCanvasEraserMode EraserMode => _eraserMode ??= new AvaloniaSkiaInkCanvasEraserMode(this);
+
+ private AvaloniaSkiaInkCanvasEraserMode? _eraserMode;
+
+ public void WritingStart()
+ {
+ if (_contextDictionary.Count > 0)
+ {
+ Log.Warn($"[AvaSkiaInkCanvas][WritingStart] 开始写的时候发现上次书写存在点没有结束 {string.Join(';', _contextDictionary.Keys)}");
+ // 兼容性处理,如果上次书写没有结束,那就清空好了
+ _contextDictionary.Clear();
+ }
+
+ if (Context.ShouldUseBitmapCache)
+ {
+ // 开始书写时,禁止使用位图缓存
+ // 防止业务端忘记关闭位图缓存,导致动态笔迹无法正确显示
+ // 由于 UseBitmapCache 里面包含一次 lock 锁,为了性能考虑,这里先判断状态再调用
+ UseBitmapCache(false);
+ }
+ }
+
+ public void WritingDown(in InkingModeInputArgs args)
+ {
+ EnsureInputConflicts();
+
+ var dynamicStrokeContext = new DynamicStrokeContext(args, this);
+ _contextDictionary[args.Id] = dynamicStrokeContext;
+ dynamicStrokeContext.Stroke.AddPoint(args.StylusPoint);
+
+ InvalidateVisual();
+ }
+
+ public void WritingMove(in InkingModeInputArgs args)
+ {
+ EnsureInputConflicts();
+
+ if (_contextDictionary.TryGetValue(args.Id, out var context))
+ {
+ context.Stroke.AddPoint(args.StylusPoint);
+ InvalidateVisual();
+ }
+ }
+
+ public void WritingUp(in InkingModeInputArgs args)
+ {
+ EnsureInputConflicts();
+
+ if (_contextDictionary.Remove(args.Id, out var context))
+ {
+ context.Stroke.AddPoint(args.StylusPoint);
+ //_staticStrokeDictionary[context.Stroke.Id] = context.Stroke;
+ context.Stroke.SetAsStatic();
+ _staticStrokeList.Add(context.Stroke);
+
+ StrokeCollected?.Invoke(this, new AvaloniaSkiaInkCanvasStrokeCollectedEventArgs(args.Id, context.Stroke));
+ }
+ InvalidateVisual();
+ }
+
+ public event EventHandler? StrokeCollected;
+
+ public void WritingCompleted()
+ {
+ Debug.Assert(_contextDictionary.Count == 0, "书写完成时,不应该有未抬起的点");
+ _contextDictionary.Clear();
+ }
+
+ ///
+ /// 现在正在写的过程中的字典
+ ///
+ private readonly Dictionary _contextDictionary = [];
+
+#if DEBUG
+ private int _count;
+ private readonly List _list = [];
+#endif
+
+ ///
+ /// 静态笔迹列表
+ ///
+ public IReadOnlyList StaticStrokeList => _staticStrokeList;
+
+ ///
+ /// 用作静态笔迹层的笔迹列表
+ ///
+ private readonly List _staticStrokeList = [];
+
+ public void AddStaticStroke(SkiaStroke skiaStroke)
+ {
+ skiaStroke.EnsureIsStaticStroke();
+ if (skiaStroke.InkCanvas != null)
+ {
+ // 禁止一个笔迹被添加到多个画布中
+ throw new InvalidOperationException("Stroke must not be added to multiple InkCanvas instances.");
+ }
+
+ _staticStrokeList.Add(skiaStroke);
+ skiaStroke.InkCanvas = this;
+ InvalidateVisual();
+ }
+
+ public void RemoveStaticStroke(SkiaStroke skiaStroke)
+ {
+ skiaStroke.EnsureIsStaticStroke();
+
+ if (!ReferenceEquals(skiaStroke.InkCanvas, this))
+ {
+ throw new InvalidOperationException("Stroke must be removed from this InkCanvas before removing.");
+ }
+
+ _staticStrokeList.Remove(skiaStroke);
+ skiaStroke.InkCanvas = null;
+
+ // 删除笔迹时,关闭位图缓存。这是因为可能存在以下情况:
+ // 1. 第一次绘制时,笔迹 A 和 B 都在,此时有前置的渲染正在进入等待
+ // 2. 用户删除了笔迹 A,设置缓存失效
+ // 3. 此时渲染线程执行第一次绘制,获取的信息是笔迹 A 和 B 都在,生成了缓存
+ // 4. 第二次绘制时,使用了缓存,笔迹 A 仍然显示
+ // 此逻辑无法规避,只能直接在删除笔迹时关闭位图缓存
+ UseBitmapCache(false);
+
+ InvalidateVisual();
+ }
+
+ internal void AddStaticStrokeWithRenderSynchronizer(SkiaStrokeRenderSynchronizer renderSynchronizer)
+ {
+ Context.UseBitmapCache(false);
+ _renderSynchronizerList.Add(renderSynchronizer);
+ _staticStrokeList.AddRange(renderSynchronizer.StrokeList);
+ foreach (var skiaStroke in renderSynchronizer.StrokeList)
+ {
+ skiaStroke.InkCanvas = this;
+ }
+ InvalidateVisual();
+
+#if DEBUG
+ foreach (var skiaStroke in _staticStrokeList)
+ {
+ Debug.Assert(skiaStroke != null);
+ }
+#endif
+
+ //#if DEBUG
+ // foreach (var skiaStroke in renderSynchronizer.StrokeList)
+ // {
+ // DumpStrokeData(skiaStroke);
+ // }
+ //#endif
+ }
+
+ ///
+ /// 调试用,输出笔迹数据到文件,文件放在桌面
+ ///
+ ///
+ private void DumpStrokeData(SkiaStroke skiaStroke)
+ {
+ var folder = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), nameof(AvaloniaSkiaInkCanvas));
+ Directory.CreateDirectory(folder);
+ var fileName = Path.Join(folder, $"{DateTime.Now:yyMMdd_HH-mm-ss} {skiaStroke.Id.Value}.txt");
+ using var streamWriter = new StreamWriter(fileName, append: true);
+ streamWriter.WriteLine($"Id: {skiaStroke.Id}");
+ streamWriter.WriteLine($"Color: {skiaStroke.Color}");
+ streamWriter.WriteLine($"InkThickness: {skiaStroke.InkThickness}");
+ streamWriter.WriteLine($"Path: {skiaStroke.Path.ToSvgPathData()}");
+ streamWriter.WriteLine($"PointCount: {skiaStroke.PointList.Count}");
+ streamWriter.WriteLine("PointList: ");
+ foreach (var point in skiaStroke.PointList)
+ {
+ streamWriter.WriteLine($"{point.X},{point.Y}");
+ }
+ }
+
+ ///
+ /// 渲染用的同步列表
+ ///
+ private readonly List _renderSynchronizerList = [];
+
+ internal void ResetStaticStrokeListByEraserResult(IEnumerable skiaStrokeList)
+ {
+ _staticStrokeList.Clear();
+ _staticStrokeList.AddRange(skiaStrokeList);
+ foreach (var skiaStroke in _staticStrokeList)
+ {
+#if DEBUG
+ skiaStroke.EnsureIsStaticStroke();
+#endif
+ skiaStroke.InkCanvas = this;
+ }
+ InvalidateVisual();
+ }
+
+ public override void Render(DrawingContext context)
+ {
+#if DEBUG
+ foreach (var skiaStroke in _staticStrokeList)
+ {
+ Debug.Assert(skiaStroke != null);
+ }
+#endif
+
+ if (_eraserMode?.IsErasing is true)
+ {
+ _eraserMode.Render(context);
+ return;
+ }
+
+ foreach (var skiaStroke in _staticStrokeList)
+ {
+ if (skiaStroke.InkCanvas is null)
+ {
+ skiaStroke.InkCanvas = this;
+ }
+ }
+
+#if DEBUG
+ _count++;
+ var n = Math.Sin(Math.Pow(Math.E * _count, Math.PI));
+ var x = Math.Abs(n) * Bounds.Width;
+ _count++;
+ n = Math.Sin(Math.Pow(Math.E * _count, Math.PI));
+ var y = Math.Abs(n) * Bounds.Height;
+
+ _list.Add(new Rect(x, y, 10, 10));
+#endif
+
+ UpdateCacheCore();
+ var inkCanvasCustomDrawOperation = new InkCanvasCustomDrawOperation(this, _cache);
+ context.Custom(inkCanvasCustomDrawOperation);
+
+ if (ElementComposition.GetElementVisual(this) is { } selfVisual)
+ {
+ Compositor compositor = selfVisual.Compositor;
+ CompositionBatch batch = compositor.RequestCompositionBatchCommitAsync();
+ batch.Rendered.ContinueWith(_ =>
+ {
+#if DEBUG // 实际测试不会进入此分支,也就是 Avalonia 的 Render 已经跑完了,但就不知道为什么还没有真的 commit 渲染画面到 DWM 那边,导致 Avalonia 还是落后一个帧。于是 WPF 这边就愉快先消失了,再等一个帧到 Avalonia 显示笔迹出来,表现就是闪烁一下
+ if (!inkCanvasCustomDrawOperation.IsFinishRender)
+ {
+ // 不再抛出,最小化时,会进入此分支,此时还是预期的
+ //throw new Exception($"笔迹开始通知退出时,还没有完成一次渲染!!! 仅调试下抛出");
+ }
+#endif
+
+ var list = inkCanvasCustomDrawOperation.CurrentRenderSynchronizerList;
+ foreach (var skiaStrokeRenderSynchronizer in list)
+ {
+ skiaStrokeRenderSynchronizer.OnRender();
+ }
+ });
+ }
+ }
+
+ ///
+ /// 更新笔迹位图缓存所需的一些上下文信息。
+ ///
+ /// 用户可见区域的边界。根坐标系。
+ /// 笔迹变换到根(通常是画板,要求画板相对于顶级窗口没有额外变换)的变换矩阵。
+ public void UpdateInkTransform(BoundingBox2D visibleBounds, SimilarityTransformation2D transformToRoot)
+ {
+ _inkTransformContext = (visibleBounds, transformToRoot);
+ }
+
+ ///
+ /// 更新位图缓存的状态。如果没有开启位图缓存,则会关闭位图缓存。
+ ///
+ private void UpdateCacheCore()
+ {
+ var scale = GetRenderScaling();
+ if (_cache.UseCacheOnNextRender is false && Context.ShouldUseBitmapCache)
+ {
+ if (_inkTransformContext is { } inkContext)
+ {
+ _cache.UpdateCacheContext(scale, inkContext.VisibleBounds, inkContext.TransformToRoot);
+ }
+ else
+ {
+ Log.Warn("[Ink][AvaSkiaInkCanvas][UpdateCacheCore] 未设置 InkTransformContext,无法更新缓存。请由笔迹元素调用 UpdateInkTransform 方法更新之。");
+ }
+ _cache.UseCacheOnNextRender = true;
+ }
+ else if (_cache.UseCacheOnNextRender && !Context.ShouldUseBitmapCache)
+ {
+ _cache.UseCacheOnNextRender = false;
+ }
+ }
+
+ class InkCanvasCustomDrawOperation : ICustomDrawOperation
+ {
+ private readonly InkBitmapCache _cache;
+
+ public InkCanvasCustomDrawOperation(AvaloniaSkiaInkCanvas inkCanvas, InkBitmapCache cache)
+ {
+ _cache = cache;
+ var contextDictionary = inkCanvas._contextDictionary;
+ _list = [];
+ _pathList = [];
+
+ foreach (var skiaStroke in inkCanvas._staticStrokeList)
+ {
+ var skiaStrokeDrawContext = skiaStroke.CreateDrawContext();
+ _pathList.Add(skiaStrokeDrawContext);
+ }
+
+ foreach (var strokeContext in contextDictionary.Values)
+ {
+ var stroke = strokeContext.Stroke;
+
+ var skiaStrokeDrawContext = stroke.CreateDrawContext();
+ _pathList.Add(skiaStrokeDrawContext);
+ }
+
+ foreach (var skiaStrokeDrawContext in _pathList)
+ {
+ _list.Add(skiaStrokeDrawContext.DrawBounds);
+ }
+
+ _currentRenderSynchronizerList = [];
+ foreach (var renderSynchronizer in inkCanvas._renderSynchronizerList)
+ {
+ bool canAdd = renderSynchronizer.StrokeList.All(skiaStroke => inkCanvas._staticStrokeList.Contains(skiaStroke));
+
+ if (canAdd)
+ {
+ _currentRenderSynchronizerList.Add(renderSynchronizer);
+ }
+ }
+
+ foreach (var skiaStrokeRenderSynchronizer in _currentRenderSynchronizerList)
+ {
+ inkCanvas._renderSynchronizerList.Remove(skiaStrokeRenderSynchronizer);
+ }
+
+#if DEBUG
+ if (_list.Count == 0)
+ {
+ _list = inkCanvas._list;
+ }
+#endif
+ if (_list.Count == 0)
+ {
+ // 如果没有笔迹,那就不需要绘制
+ // 设置 Bounds 为 0 将在 Render 中不绘制
+ Bounds = new Rect(0, 0, 0, 0);
+ // 为了防止闪烁,在外层当前渲染次数结束后再通知渲染完成,因此这里不通知渲染完成
+ //// 由于在 Render 中不绘制,所以需要先通知渲染完成
+ //foreach (var skiaStrokeRenderSynchronizer in _currentRenderSynchronizerList)
+ //{
+ // skiaStrokeRenderSynchronizer.OnRender();
+ //}
+ return;
+ }
+
+ var list = _list;
+
+ Rect bounds = list[0];
+ for (var i = 1; i < list.Count; i++)
+ {
+ bounds = bounds.Union(list[i]);
+ }
+ Bounds = bounds;
+
+ // 理论上 inkCanvas._renderSynchronizerList 是零个
+ Log.Debug($"[Ink][AvaSkiaInkCanvas] InkCanvasCustomDrawOperation 正常执行={inkCanvas._renderSynchronizerList.Count == 0}");
+ }
+
+ public IReadOnlyList CurrentRenderSynchronizerList => _currentRenderSynchronizerList;
+ private readonly List _currentRenderSynchronizerList;
+ private List _list;
+ private List _pathList;
+
+ public void Dispose()
+ {
+ foreach (var skiaStrokeDrawContext in _pathList)
+ {
+ skiaStrokeDrawContext.Dispose();
+ }
+ }
+
+ public bool Equals(ICustomDrawOperation? other)
+ {
+ return false;
+ }
+
+ public bool HitTest(Point p)
+ {
+ return false;
+ }
+
+ public void Render(ImmediateDrawingContext context)
+ {
+ var skiaSharpApiLeaseFeature = context.TryGetFeature();
+ if (skiaSharpApiLeaseFeature == null)
+ {
+ return;
+ }
+
+ using var skiaSharpApiLease = skiaSharpApiLeaseFeature.Lease();
+ var canvas = skiaSharpApiLease.SkCanvas;
+ DrawCore(canvas);
+ IsFinishRender = true;
+ }
+
+ ///
+ /// 是否已经完成渲染
+ ///
+ public bool IsFinishRender { get; private set; }
+
+ private void DrawCore(SKCanvas canvas)
+ {
+ using var skPaint = new SKPaint();
+
+ skPaint.Color = SKColors.Red;
+ skPaint.Style = SKPaintStyle.Fill;
+ skPaint.IsAntialias = true;
+ skPaint.StrokeWidth = 10;
+
+ if (_cache.UseCacheOnNextRender)
+ {
+ // 当缓存可用时,使用此缓存。
+ _cache.DrawBitmap([.. _pathList], canvas, skPaint);
+ }
+ else if (_pathList.Count > 0)
+ {
+ // 当笔迹路径可用时,使用此路径。
+
+ // 绘制笔迹。
+ foreach (var skiaStrokeDrawContext in _pathList)
+ {
+ canvas.DrawStroke(in skiaStrokeDrawContext, skPaint);
+ }
+
+ //// 清除动态层的笔迹渲染,然后清除动态层。
+ //foreach (var skiaStrokeRenderSynchronizer in _currentRenderSynchronizerList)
+ //{
+ // Log.Debug($"[Ink][AvaSkiaInkCanvas] 渲染回调 OnRender");
+
+ // skiaStrokeRenderSynchronizer.OnRender();
+ //}
+ //// 防止重复渲染
+ //_currentRenderSynchronizerList.Clear();
+ }
+ else
+ {
+ // 当笔迹路径不可用时,使用此调试。
+#if DEBUG
+ // 仅供调试。
+ for (var i = 0; i < _list.Count; i++)
+ {
+ var bounds = _list[i];
+ var x = (float) bounds.X;
+ var y = (float) bounds.Y;
+
+ skPaint.Color = new SKColor((uint) (Math.Sin(Math.Pow(Math.E * i, Math.PI)) * int.MaxValue));
+
+ canvas.DrawRect(x, y, 10, 10, skPaint);
+ }
+#endif
+ }
+ }
+
+ public Rect Bounds { get; }
+ }
+
+ ///
+ /// 指定是否立即使用位图缓存替代真实的笔迹,或是使用真实的笔迹渲染。
+ ///
+ ///
+ /// 指定为 ,则立即使用位图缓存替代真实的笔迹;
+ /// 指定为 ,则位图缓存将不会工作,而使用真实的笔迹渲染。
+ ///
+ ///
+ /// 注意,此方法并不会修改用户设置的位图缓存开关,而是设置在某种程序状态下应该打开还是关闭位图缓存。
+ /// 关于它们之间的区别,请参见 的注释。
+ ///
+ public void UseBitmapCache(bool useBitmapCache)
+ {
+ Context.UseBitmapCache(useBitmapCache);
+ UpdateBitmapCache();
+ }
+
+ ///
+ /// 使笔迹的位图缓存失效,并立即重新生成缓存。
+ ///
+ public void UpdateBitmapCache()
+ {
+ InvalidateBitmapCache();
+ InvalidateVisual();
+ }
+
+ ///
+ /// 使笔迹的位图缓存失效。下次绘制时,如果位图缓存启用,则会重新生成缓存。
+ ///
+ public void InvalidateBitmapCache()
+ {
+ var scale = GetRenderScaling();
+ if (_inkTransformContext is { } inkContext)
+ {
+ _cache.UpdateCacheContext(scale, inkContext.VisibleBounds, inkContext.TransformToRoot);
+ }
+ else
+ {
+ Log.Warn("[Ink][AvaSkiaInkCanvas][InvalidateBitmapCache] 未设置 InkTransformContext,无法更新缓存。请由笔迹元素调用 UpdateInkTransform 方法更新之。");
+ }
+ }
+
+ private double GetRenderScaling()
+ {
+ return TopLevel.GetTopLevel(this)?.RenderScaling ?? 1;
+ }
+
+ private bool IsWriting => _contextDictionary.Count > 0;
+
+ internal void EnsureInputConflicts()
+ {
+ if (IsWriting && EraserMode.IsErasing)
+ {
+ throw new InvalidOperationException("Writing and erasing cannot be performed at the same time.");
+ }
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Core/SkiaStroke.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Core/SkiaStroke.cs
new file mode 100644
index 0000000..aa8a51d
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Core/SkiaStroke.cs
@@ -0,0 +1,296 @@
+using System.Diagnostics.CodeAnalysis;
+using Avalonia;
+using Avalonia.Skia;
+using DotNetCampus.Inking.Contexts;
+using DotNetCampus.Inking.Primitive;
+using DotNetCampus.Inking.StrokeRenderers;
+using DotNetCampus.Inking.Utils;
+using DotNetCampus.Numerics.Geometry;
+
+using SkiaSharp;
+
+namespace DotNetCampus.Inking;
+
+public class SkiaStroke : IDisposable
+{
+ public SkiaStroke(InkId id) : this(id, new SKPath(), ownSkiaPath: true)
+ {
+ }
+
+ private SkiaStroke(InkId id, SKPath path, bool ownSkiaPath)
+ {
+ _ownSkiaPath = ownSkiaPath;
+ Id = id;
+ Path = path;
+ }
+
+ ///
+ /// 笔迹渲染器
+ ///
+ public ISkiaInkStrokeRenderer? InkStrokeRenderer { get; init; }
+
+ public AvaloniaSkiaInkCanvas? InkCanvas { get; set; }
+
+ public InkId Id { get; }
+
+ public SKPath Path { get; private set; }
+
+ public SKMatrix Transform { get; private set; } = SKMatrix.Identity;
+
+ ///
+ /// 是否拥有 的所有权,即需要在释放的使用同步将其释放
+ ///
+ private readonly bool _ownSkiaPath;
+
+ ///
+ /// 笔迹颜色
+ ///
+ public SKColor Color { get; init; } = AvaloniaSkiaInkCanvasSettings.DefaultInkColor;
+
+ ///
+ /// 笔迹粗细
+ ///
+ public float InkThickness { get; init; } = AvaloniaSkiaInkCanvasSettings.DefaultInkThickness;
+
+ internal IReadOnlyList PointList => _pointList;
+
+ ///
+ /// 是否忽略压感
+ ///
+ public bool IgnorePressure { get; init; }
+
+ private readonly List _pointList = [];
+
+ public void AddPoint(InkStylusPoint point) => AddPoints([point]);
+
+ public void AddPoints(in IEnumerable points)
+ {
+ if (_isStaticStroke)
+ {
+ throw new InvalidOperationException($"禁止修改静态笔迹的点");
+ }
+
+ InkStylusPoint? lastPoint = _pointList.Count > 0 ? PointList[^1] : default;
+ foreach (InkStylusPoint currentPoint in points)
+ {
+ InkStylusPoint point = currentPoint;
+
+ if (IgnorePressure)
+ {
+ point = point with
+ {
+ Pressure = InkStylusPoint.DefaultPressure,
+ };
+ }
+
+ if (lastPoint == point)
+ {
+ // 如果两个点相同,则丢点
+ continue;
+ }
+
+ lastPoint = point;
+
+ _pointList.Add(point);
+ }
+
+ var pointList = _pointList;
+ if (InkCanvas?.Settings.ShouldReCreatePoint is true && pointList.Count > 10)
+ {
+ pointList = ApplyMeanFilter(pointList);
+ }
+
+ RenderInk(pointList);
+ }
+
+ [AllowNull]
+ internal SkiaSimpleInkRender SimpleInkRender
+ {
+ get
+ {
+ return _skiaSimpleInkRender ??= new SkiaSimpleInkRender();
+ }
+ init
+ {
+ _skiaSimpleInkRender = value;
+ }
+ }
+
+ private SkiaSimpleInkRender? _skiaSimpleInkRender;
+
+ private void RenderInk(List pointList)
+ {
+ if (InkStrokeRenderer is not null)
+ {
+ // 如果有传入渲染器,则使用传入的渲染器
+ Path.Dispose();
+ Path = InkStrokeRenderer.RenderInkToPath(pointList, InkThickness);
+ }
+ else
+ {
+ if (pointList.Count >= 2)
+ {
+ var outlinePointList = SimpleInkRender.GetOutlineSKPointList(pointList, InkThickness);
+
+ Path.Reset();
+ Path.AddPoly(outlinePointList);
+ }
+ else if (pointList.Count == 1)
+ {
+ Path.Reset();
+ var stylusPoint = pointList[0];
+ float x = (float) stylusPoint.X;
+ float y = (float) stylusPoint.Y;
+ // 如果是一个点,那就画一个圆。圆的半径就是 笔迹粗细 * 压力 / 2
+ // 为什么要除以 2,因为传入的是半径
+ var radius = InkThickness * stylusPoint.Pressure / 2;
+ Path.AddCircle(x, y, radius);
+ }
+ else
+ {
+ // 一个点都没有,那就什么都不画
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_ownSkiaPath)
+ {
+ Path.Dispose();
+ }
+ }
+
+ public static List ApplyMeanFilter(List pointList, int step = 10)
+ {
+ var xList = ApplyMeanFilter(pointList.Select(t => t.Point.X).ToList(), step);
+ var yList = ApplyMeanFilter(pointList.Select(t => t.Point.Y).ToList(), step);
+
+ var newPointList = new List();
+ for (int i = 0; i < xList.Count && i < yList.Count; i++)
+ {
+ newPointList.Add(new InkStylusPoint(xList[i], yList[i]));
+ }
+
+ return newPointList;
+ }
+
+ ///
+ /// 滤波算法,细节请看 [WPF 记一个特别简单的点集滤波平滑方法 - lindexi - 博客园](https://www.cnblogs.com/lindexi/p/18387840 )
+ ///
+ ///
+ ///
+ ///
+ public static List ApplyMeanFilter(List list, int step)
+ {
+ // 前面一半加不了
+ var newList = new List(list.Take(step / 2));
+ for (int i = step / 2; i < list.Count - step + step / 2; i++)
+ {
+ // 当前点,取前后各一半,即 step / 2 个点,求平均值作为当前点的值
+ newList.Add(list.Skip(i - step / 2).Take(step).Sum() / step);
+ }
+ // 后面一半加不了
+ newList.AddRange(list.Skip(list.Count - (step - step / 2)));
+ return newList;
+ }
+
+ internal SkiaStrokeDrawContext CreateDrawContext()
+ {
+ SKPath skPath;
+ bool shouldDisposePath;
+ if (_isStaticStroke)
+ {
+ // 静态笔迹,不需要复制,因为不会再更改,不存在线程安全问题
+ skPath = Path;
+ // 静态笔迹不需要释放,释放了会导致绘制闪退
+ shouldDisposePath = false;
+ }
+ else
+ {
+ // 动态笔迹,需要复制,因为可能会在多个线程中绘制使用和释放
+ // 如在 UI 线程加点,修改 Path 内容。与此同时在渲染线程绘制,导致多线程同时访问
+ // 为了避免这种情况,复制 Path 解决线程安全问题
+ skPath = Path.Clone();
+ shouldDisposePath = true;
+ }
+
+ return new SkiaStrokeDrawContext(Color, skPath, GetDrawBounds(), Transform, shouldDisposePath);
+ }
+
+ internal void SetAsStatic()
+ {
+ _drawBounds = GetDrawBounds();
+ _isStaticStroke = true;
+ // 不再需要渲染了,释放渲染器
+ _skiaSimpleInkRender = null;
+ }
+
+ public static SkiaStroke CreateStaticStroke(InkId id, SKPath path, StylusPointListSpan pointList, SKColor color,
+ float inkThickness, bool ownSkiaPath, ISkiaInkStrokeRenderer? inkStrokeRenderer)
+ {
+ var skiaStroke = new SkiaStroke(id, path, ownSkiaPath)
+ {
+ Color = color,
+ InkThickness = inkThickness,
+ InkStrokeRenderer = inkStrokeRenderer,
+ };
+
+ skiaStroke._pointList.EnsureCapacity(pointList.Length);
+ skiaStroke._pointList.AddRange(pointList.GetEnumerable());
+ skiaStroke.SetAsStatic();
+
+ return skiaStroke;
+ }
+
+ private bool _isStaticStroke;
+ private Rect _drawBounds;
+
+ public Rect GetDrawBounds()
+ {
+ if (_isStaticStroke)
+ {
+ return _drawBounds;
+ }
+
+ return SkiaSharpExtensions.ToAvaloniaRect(Path.Bounds).ExpandLength(InkThickness);
+ }
+
+ public void SetTransform(SKMatrix matrix)
+ {
+ Transform = matrix;
+ InkCanvas?.InvalidateVisual();
+ }
+
+ public void ApplyTransform(SimilarityTransformation2D transform)
+ {
+ for (var i = 0; i < _pointList.Count; i++)
+ {
+ var point = _pointList[i];
+ _pointList[i] = new InkStylusPoint(transform.Transform(point.Point), point.Pressure);
+ }
+
+ Path = new SKPath();
+
+ if (_pointList.Count > 2)
+ {
+ var outlinePointList = SimpleInkRender.GetOutlineSKPointList(_pointList, InkThickness);
+
+ Path.Reset();
+ Path.AddPoly(outlinePointList);
+ }
+
+ Transform = SKMatrix.Identity;
+ _drawBounds = SkiaSharpExtensions.ToAvaloniaRect(Path.Bounds).ExpandLength(InkThickness);
+
+ InkCanvas?.InvalidateVisual();
+ }
+
+ public void EnsureIsStaticStroke()
+ {
+ if (!_isStaticStroke)
+ {
+ throw new InvalidOperationException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/DotNetCampus.AvaloniaInkCanvas.csproj b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/DotNetCampus.AvaloniaInkCanvas.csproj
new file mode 100644
index 0000000..0617961
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/DotNetCampus.AvaloniaInkCanvas.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net8.0
+ enable
+ enable
+ DotNetCampus.Inking
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/DotNetCampus.AvaloniaInkCanvas.csproj.DotSettings b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/DotNetCampus.AvaloniaInkCanvas.csproj.DotSettings
new file mode 100644
index 0000000..231aea2
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/DotNetCampus.AvaloniaInkCanvas.csproj.DotSettings
@@ -0,0 +1,4 @@
+
+ True
+ True
+ True
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/AvaloniaSkiaInkCanvasEraserMode.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/AvaloniaSkiaInkCanvasEraserMode.cs
new file mode 100644
index 0000000..eb0ae90
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/AvaloniaSkiaInkCanvasEraserMode.cs
@@ -0,0 +1,280 @@
+using System.Diagnostics;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Skia;
+using DotNetCampus.Inking.Contexts;
+using DotNetCampus.Inking.Interactives;
+using DotNetCampus.Inking.Utils;
+using SkiaSharp;
+using Point = Avalonia.Point;
+using Rect = Avalonia.Rect;
+using Size = Avalonia.Size;
+
+namespace DotNetCampus.Inking.Erasing;
+
+public class AvaloniaSkiaInkCanvasEraserMode
+{
+ public AvaloniaSkiaInkCanvasEraserMode(AvaloniaSkiaInkCanvas inkCanvas)
+ {
+ InkCanvas = inkCanvas;
+ }
+
+ private void InkCanvas_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
+ {
+ _debugEraserSizeScale += e.Delta.Y;
+ }
+
+ private double _debugEraserSizeScale = 0;
+
+ public AvaloniaSkiaInkCanvas InkCanvas { get; }
+ private AvaloniaSkiaInkCanvasSettings Settings => InkCanvas.Settings;
+
+ public bool IsErasing { get; private set; }
+ private int MainEraserInputId { set; get; }
+
+ private PointPathEraserManager PointPathEraserManager { get; } = new PointPathEraserManager();
+
+ private IEraserView EraserView
+ {
+ get
+ {
+ if (_eraserView is null)
+ {
+ var eraserViewCreator = Settings.EraserViewCreator;
+ _eraserView = eraserViewCreator?.CreateEraserView()
+ ?? new EraserView();
+ }
+
+ return _eraserView;
+ }
+ }
+
+ private IEraserView? _eraserView;
+
+ private void StartEraser()
+ {
+#if DEBUG
+ var topLevel = TopLevel.GetTopLevel(InkCanvas)!;
+ topLevel.PointerWheelChanged -= InkCanvas_PointerWheelChanged;
+ topLevel.PointerWheelChanged += InkCanvas_PointerWheelChanged;
+#endif
+
+ var staticStrokeList = InkCanvas.StaticStrokeList;
+ PointPathEraserManager.StartEraserPointPath(staticStrokeList);
+
+ // 如果没有自定义渲染器,则使用简单渲染器
+ if (InkCanvas.Settings.InkStrokeRenderer is null)
+ {
+ PointPathEraserManager.SimpleInkRender = InkCanvas.SimpleInkRender;
+ }
+
+ if (EraserView is Control eraserView)
+ {
+ InkCanvas.AddChild(eraserView);
+ }
+
+ _inputProcessStopwatch.Restart();
+ _lastEraserSize = Settings.EraserSize;
+ }
+
+ private readonly Stopwatch _inputProcessStopwatch = new();
+
+ public void EraserDown(in InkingModeInputArgs args)
+ {
+ InkCanvas.EnsureInputConflicts();
+ if (!IsErasing)
+ {
+ MainEraserInputId = args.Id;
+
+ IsErasing = true;
+
+ StartEraser();
+
+ EraserView.SetEraserSize(Settings.EraserSize);
+ EraserView.Move(args.Position.ToAvaloniaPoint());
+ InkCanvas.InvalidateVisual();
+ }
+ else
+ {
+ // 忽略其他的输入点
+ }
+ }
+
+ private Size _lastEraserSize;
+
+ public void EraserMove(in InkingModeInputArgs args)
+ {
+ InkCanvas.EnsureInputConflicts();
+ if (IsErasing && args.Id == MainEraserInputId)
+ {
+ // 擦除
+ var eraserWidth = _lastEraserSize.Width;
+ var eraserHeight = _lastEraserSize.Height;
+
+ if (Settings.EnableStylusSizeAsEraserSize)
+ {
+ var touchWidth = args.StylusPoint.Width ?? eraserWidth;
+ var touchHeight = args.StylusPoint.Height ?? eraserHeight;
+
+ if (Settings.CanEraserAlwaysFollowsTouchSize || _inputProcessStopwatch.Elapsed < Settings.EraserCanResizeDuringTimeSpan)
+ {
+ eraserWidth = touchWidth;
+ eraserHeight = touchHeight;
+ }
+ }
+
+#if DEBUG
+ if (_debugEraserSizeScale > 0)
+ {
+ _debugEraserSizeScale = Math.Min(100, _debugEraserSizeScale);
+
+ eraserWidth *= (1 + _debugEraserSizeScale / 10);
+ eraserHeight *= (1 + _debugEraserSizeScale / 10);
+ }
+ else if (_debugEraserSizeScale < -10)
+ {
+ _debugEraserSizeScale = Math.Max(-100, _debugEraserSizeScale);
+
+ eraserWidth *= (1 + _debugEraserSizeScale / 100);
+ eraserHeight *= (1 + _debugEraserSizeScale / 100);
+ }
+#endif
+
+ if (Settings.LockMinEraserSize)
+ {
+ // 锁定最小橡皮擦
+ // 有人嫌弃小咯,那就改大点咯
+ eraserWidth = Math.Max(eraserWidth, Settings.MinEraserSize.Width);
+ eraserHeight = Math.Max(eraserHeight, Settings.MinEraserSize.Height);
+ }
+
+ // 限制最大橡皮擦,防止那些 SB 设备报告的宽度过大
+ eraserWidth = Math.Min(eraserWidth, Settings.MaxEraserSize.Width);
+ eraserHeight = Math.Min(eraserHeight, Settings.MaxEraserSize.Height);
+
+ var rect = new Rect(args.Position.X - eraserWidth / 2, args.Position.Y - eraserHeight / 2, eraserWidth, eraserHeight);
+ PointPathEraserManager.Move(rect.ToRect2D());
+
+ var eraserSize = new Size(eraserWidth, eraserHeight);
+ _lastEraserSize = eraserSize;
+ EraserView.SetEraserSize(eraserSize);
+ EraserView.Move(args.Position.ToAvaloniaPoint());
+ InkCanvas.InvalidateVisual();
+ }
+ }
+
+ public void EraserUp(in InkingModeInputArgs args)
+ {
+ InkCanvas.EnsureInputConflicts();
+ if (IsErasing && args.Id == MainEraserInputId)
+ {
+ IsErasing = false;
+ var pointPathEraserResult = PointPathEraserManager.Finish();
+
+ var skiaStrokeList = pointPathEraserResult.ErasingSkiaStrokeList
+ .SelectMany(t => t.IsErased
+ ? t.NewStrokeList // 被擦掉的,使用新的笔迹列表替代
+ : [t.OriginStroke]); // 没有被擦掉的,使用原笔迹
+
+ InkCanvas.ResetStaticStrokeListByEraserResult(skiaStrokeList);
+
+ ClearEraser();
+
+ ErasingCompleted?.Invoke(this, new ErasingCompletedEventArgs(pointPathEraserResult.ErasingSkiaStrokeList));
+ }
+ }
+
+ private void ClearEraser()
+ {
+ if (EraserView is Control eraserView)
+ {
+ InkCanvas.RemoveChild(eraserView);
+ }
+ }
+
+ public event EventHandler? ErasingCompleted;
+
+ public void Render(DrawingContext context)
+ {
+ context.Custom(new EraserModeCustomDrawOperation(this));
+ }
+
+ class EraserModeCustomDrawOperation : ICustomDrawOperation
+ {
+ public EraserModeCustomDrawOperation(AvaloniaSkiaInkCanvasEraserMode eraserMode)
+ {
+ var pointPathEraserManager = eraserMode.PointPathEraserManager;
+ IReadOnlyList drawContextList = pointPathEraserManager.GetDrawContextList();
+ DrawContextList = drawContextList;
+
+ if (drawContextList.Count == 0)
+ {
+ Bounds = new Rect(0, 0, 0, 0);
+ }
+ else
+ {
+ Rect bounds = drawContextList[0].DrawBounds;
+
+ for (var i = 1; i < drawContextList.Count; i++)
+ {
+ bounds = bounds.Union(drawContextList[i].DrawBounds);
+ }
+
+ Bounds = bounds;
+ }
+ }
+
+ private IReadOnlyList DrawContextList { get; }
+
+ public void Dispose()
+ {
+ foreach (var skiaStrokeDrawContext in DrawContextList)
+ {
+ skiaStrokeDrawContext.Dispose();
+ }
+ }
+
+ public bool Equals(ICustomDrawOperation? other)
+ {
+ return false;
+ }
+
+ public bool HitTest(Point p)
+ {
+ return false;
+ }
+
+ public void Render(ImmediateDrawingContext context)
+ {
+ var skiaSharpApiLeaseFeature = context.TryGetFeature();
+ if (skiaSharpApiLeaseFeature == null)
+ {
+ return;
+ }
+
+ using var skiaSharpApiLease = skiaSharpApiLeaseFeature.Lease();
+ var canvas = skiaSharpApiLease.SkCanvas;
+
+ using var skPaint = new SKPaint();
+ skPaint.Color = SKColors.Red;
+ skPaint.Style = SKPaintStyle.Fill;
+
+ skPaint.IsAntialias = true;
+
+ skPaint.StrokeWidth = 10;
+
+ foreach (var drawContext in DrawContextList)
+ {
+ // 绘制
+ skPaint.Color = drawContext.Color;
+ canvas.DrawPath(drawContext.Path, skPaint);
+ }
+ }
+
+ public Rect Bounds { get; }
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/ErasedSkiaStroke.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/ErasedSkiaStroke.cs
new file mode 100644
index 0000000..f87ee37
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/ErasedSkiaStroke.cs
@@ -0,0 +1,35 @@
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+
+namespace DotNetCampus.Inking.Erasing;
+
+///
+/// 橡皮擦掉之后的笔迹
+///
+public readonly record struct ErasedSkiaStroke
+{
+ public ErasedSkiaStroke(SkiaStroke originStroke, IReadOnlyList? newStrokeList, bool isErased)
+ {
+ OriginStroke = originStroke;
+ NewStrokeList = newStrokeList;
+ IsErased = isErased;
+
+ Debug.Assert(isErased == (newStrokeList != null), "被擦掉的情况下,必定存在列表,即使是空列表");
+ }
+
+ ///
+ /// 原始的笔迹
+ ///
+ public SkiaStroke OriginStroke { get; }
+
+ ///
+ /// 被擦掉之后的新笔迹列表,可能为空列表。空列表和 null 有区别,空列表表示被擦掉了,但是没有新的笔迹,而 null 表示没被擦掉
+ ///
+ public IReadOnlyList? NewStrokeList { get; }
+
+ ///
+ /// 是否被擦掉了,即 被擦掉成多条 新的笔迹
+ ///
+ [MemberNotNullWhen(true, nameof(NewStrokeList))]
+ public bool IsErased { get; }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/PointPathEraserManager.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/PointPathEraserManager.cs
new file mode 100644
index 0000000..0f731a7
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/PointPathEraserManager.cs
@@ -0,0 +1,398 @@
+using System.Diagnostics;
+
+using Avalonia.Skia;
+
+using DotNetCampus.Inking.Contexts;
+using DotNetCampus.Inking.Primitive;
+using DotNetCampus.Numerics;
+using DotNetCampus.Numerics.Geometry;
+
+using SkiaSharp;
+
+using Point2D = DotNetCampus.Numerics.Geometry.Point2D;
+
+namespace DotNetCampus.Inking.Erasing;
+
+///
+/// 点擦路径擦除
+///
+class PointPathEraserManager
+{
+ private bool _isErasing;
+
+ public void StartEraserPointPath(IReadOnlyList staticStrokeList)
+ {
+ Debug.Assert(_isErasing == false, $"开始橡皮擦时,开始橡皮擦的 {nameof(_isErasing)} 字段状态一定为 false 值");
+ Debug.Assert(WorkList.Count == 0, "橡皮擦计算开始的时候,必然此时没有任何笔迹正在被橡皮擦工作中");
+ // 兜底代码,确保 WorkList 为空,防止重复加入导致进入诡异的逻辑
+ WorkList.Clear();
+
+ _isErasing = true;
+
+ WorkList.EnsureCapacity(staticStrokeList.Count);
+ var workList = WorkList;
+ foreach (var skiaStrokeSynchronizer in staticStrokeList)
+ {
+ workList.Add(new InkInfoForEraserPointPath(skiaStrokeSynchronizer));
+ }
+ }
+
+ private List WorkList { get; } = [];
+ public SkiaSimpleInkRender? SimpleInkRender { get; set; }
+
+ private readonly List _cacheList = [];
+
+ public void Move(Rect2D rect) => Move(new RotatedRect(new Point2D(rect.X, rect.Y), new Size2D(rect.Width, rect.Height), AngularMeasure.Zero));
+
+ public void Move(RotatedRect rotatedEraserRect)
+ {
+ var eraserBoundingBox = rotatedEraserRect.GetBoundingBox();
+ var transformation = AffineTransformation2D.Identity.RotateAt(-rotatedEraserRect.Rotate, rotatedEraserRect.Location).Translate(-rotatedEraserRect.Location.ToVector());
+ var eraserBoundingBoxRect2D = new Rect2D(eraserBoundingBox.MinX, eraserBoundingBox.MinY, eraserBoundingBox.Width, eraserBoundingBox.Height);
+
+ foreach (InkInfoForEraserPointPath inkInfoForEraserPointPath in WorkList)
+ {
+ _cacheList.Clear();
+
+ // 擦点的核心逻辑
+ foreach (SubInkInfoForEraserPointPath pointPath in inkInfoForEraserPointPath.SubInkInfoList)
+ {
+ var bounds = pointPath.CacheBounds;
+ if (!bounds.IntersectsWith(eraserBoundingBoxRect2D))
+ {
+ _cacheList.Add(pointPath);
+ continue;
+ }
+
+ var span = pointPath.PointListSpan;
+ var start = -1;
+ var length = 0;
+
+ for (int i = 0; i < span.Length; i++)
+ {
+ var index = span.Start + i;
+ var point = inkInfoForEraserPointPath.PointList[index];
+
+ //var point = inkInfoForEraserPointPath.StrokeSynchronizer.StylusPoints[index].Point;
+ //_pointCount++;
+
+ if (
+ // 1. 短路过滤:快速判断目标点是否在旋转矩形的外接边框内(这可以过滤掉板书中的绝大部分点)。
+ eraserBoundingBox.Contains(point)
+ // 2. 精确判断:判断目标点是否在旋转矩形内。
+ // 2.1. 将目标点转换到旋转矩形的坐标系中;
+ && transformation.Transform(point) is { X: >= 0, Y: >= 0 } transformedPoint
+ // 2.2. 判断变换后的点是否在矩形内。
+ && transformedPoint.X <= rotatedEraserRect.Size.Width
+ && transformedPoint.Y <= rotatedEraserRect.Size.Height)
+ {
+ if (start != -1)
+ {
+ // 截断
+ _cacheList.Add(pointPath.Sub(start, length));
+ }
+
+ start = -1;
+ length = 0;
+ }
+ else
+ {
+ if (start == -1)
+ {
+ start = index;
+ length = 1;
+ }
+ else
+ {
+ length++;
+ }
+ }
+ }
+
+ // 这里的 start 是相对于 pointPath.PointPath 的,而不是相对于当前的 pointPath.PointListSpan 的。因此 start 为 0 不代表就是当前的 pointPath 的起点,而应该是 start == pointPath.PointListSpan.Start 才是代表起点和 pointPath 相同
+ if (start == pointPath.PointListSpan.Start && length == pointPath.PointListSpan.Length)
+ {
+ // 短路代码,表示这条笔迹一个点都没被擦掉
+ _cacheList.Add(pointPath);
+ }
+ else
+ {
+ if (start != -1)
+ {
+ // 截断
+ _cacheList.Add(pointPath.Sub(start, length));
+ }
+
+ // 截断最后需要将原来释放掉
+ pointPath.Dispose();
+ }
+ }
+
+ inkInfoForEraserPointPath.SubInkInfoList.Clear();
+ inkInfoForEraserPointPath.SubInkInfoList.AddRange(_cacheList);
+
+#if DEBUG
+ foreach (var subInkInfoForEraserPointPath in _cacheList)
+ {
+ if (subInkInfoForEraserPointPath.IsDisposed)
+ {
+ Debugger.Break();
+ }
+ }
+#endif
+
+ _cacheList.Clear();
+ }
+ }
+
+ public PointPathEraserResult Finish()
+ {
+ _isErasing = false;
+ var count = WorkList.Sum(t => t.SubInkInfoList.Count);
+ var erasingSkiaStrokeList = new List(count);
+
+ foreach (var inkInfoForEraserPointPath in WorkList)
+ {
+ var originSkiaStroke = inkInfoForEraserPointPath.OriginSkiaStroke;
+
+ IReadOnlyList? newStrokeList;
+ if (inkInfoForEraserPointPath.SubInkInfoList.Count == 0)
+ {
+ // 笔迹完全被擦掉了
+ newStrokeList = Array.Empty();
+ }
+ else if (inkInfoForEraserPointPath.IsErased)
+ {
+ var strokeList = new List(inkInfoForEraserPointPath.SubInkInfoList.Count);
+ newStrokeList = strokeList;
+
+ foreach (var subInkInfoForEraserPointPath in inkInfoForEraserPointPath.SubInkInfoList)
+ {
+ var subSpan = subInkInfoForEraserPointPath.PointListSpan;
+ var pointList = new StylusPointListSpan(originSkiaStroke.PointList, subSpan.Start, subSpan.Length);
+
+ var skPath = subInkInfoForEraserPointPath.CachePath ?? ToPath(subInkInfoForEraserPointPath);
+ // 已经从 CachePath 取出,不能再有原来的引用,生怕被释放
+ subInkInfoForEraserPointPath.CachePath = null;
+
+ var skiaStroke = SkiaStroke.CreateStaticStroke(InkId.NewId(), skPath, pointList, originSkiaStroke.Color,
+ originSkiaStroke.InkThickness, ownSkiaPath: true, originSkiaStroke.InkStrokeRenderer);
+ strokeList.Add(skiaStroke);
+ }
+ }
+ else
+ {
+ // 没被擦的笔迹依然可以使用原来的笔迹,设计上配置 newStrokeList 为空,减少对象的创建
+ // 满屏幕的笔迹,然后只擦掉一个笔迹,如果没有被擦掉的笔迹也创建 List 那将会是一个很大的开销
+ newStrokeList = null;
+ }
+
+ erasingSkiaStrokeList.Add(new ErasedSkiaStroke(originSkiaStroke, newStrokeList, inkInfoForEraserPointPath.IsErased));
+ }
+
+ WorkList.Clear();
+ var result = new PointPathEraserResult(erasingSkiaStrokeList);
+ return result;
+ }
+
+ ///
+ /// 获取渲染内容
+ ///
+ ///
+ /// 为什么获取渲染内容需要在准备渲染时才获取,而不是在擦的过程中计算? 原因是机器设备性能太差,擦的过程的进入次数会比渲染次数更多,且在插的过程中计算出来的结果没有被实际使用到,于是不如就在准备渲染的时候计算,如此可以稍微提升一些性能
+ public IReadOnlyList GetDrawContextList()
+ {
+ var count = WorkList.Sum(t => t.SubInkInfoList.Count);
+ var result = new List(count);
+
+ foreach (var inkInfoForEraserPointPath in WorkList)
+ {
+ var originSkiaStroke = inkInfoForEraserPointPath.OriginSkiaStroke;
+ if (inkInfoForEraserPointPath.IsErased)
+ {
+ // 被擦掉的笔迹,就需要逐个笔迹计算
+ foreach (var subInkInfoForEraserPointPath in inkInfoForEraserPointPath.SubInkInfoList)
+ {
+ if (subInkInfoForEraserPointPath.IsDisposed)
+ {
+ throw new ObjectDisposedException($"当前所使用的 SubInkInfoForEraserPointPath 已经被释放了,橡皮擦状态不正常");
+ }
+
+ subInkInfoForEraserPointPath.CachePath ??= ToPath(subInkInfoForEraserPointPath);
+ // 为什么需要复制一个?原因是接下来的渲染是交给 Avalonia 的渲染线程上,释放时机不固定。原本的在 UI 线程上的 CachePath 的释放时机是这条笔迹被擦到的时候释放,也不能和渲染线程统一,只好进行拷贝一次
+ var skPath = subInkInfoForEraserPointPath.CachePath.Clone();
+
+ result.Add(new SkiaStrokeDrawContext(originSkiaStroke.Color, skPath, skPath.Bounds.ToAvaloniaRect(), SKMatrix.Identity, ShouldDisposePath: true));
+ }
+ }
+ else
+ {
+ // 没被擦的笔迹依然可以使用静态笔迹提升性能
+#if DEBUG
+ originSkiaStroke.EnsureIsStaticStroke();
+#endif
+ result.Add(originSkiaStroke.CreateDrawContext());
+ }
+ }
+
+ return result;
+ }
+
+ private SKPath ToPath(SubInkInfoForEraserPointPath subInkInfoForEraserPointPath)
+ {
+ SkiaStroke originSkiaStroke = subInkInfoForEraserPointPath.PointPath.OriginSkiaStroke;
+
+ var subSpan = subInkInfoForEraserPointPath.PointListSpan;
+ // 对于 WPF 注入的渲染器,只要大于一个点就可以开始渲染了
+ if (subSpan.Length > 0)
+ {
+ var pointList = new StylusPointListSpan(originSkiaStroke.PointList, subSpan.Start, subSpan.Length);
+
+ if (originSkiaStroke.InkStrokeRenderer is { } inkStrokeRenderer)
+ {
+ return inkStrokeRenderer.RenderInkToPath(pointList.ToReadOnlyList(), originSkiaStroke.InkThickness);
+ }
+
+ // 如果没有自定义的渲染器,就使用默认的渲染器来进行渲染。默认简单渲染器要求大于两个点才能进行渲染
+ if (subSpan.Length > 2)
+ {
+ SimpleInkRender ??= new SkiaSimpleInkRender();
+ var outlinePointList = SimpleInkRender.GetOutlineSKPointList(pointList.ToReadOnlyList(), originSkiaStroke.InkThickness);
+
+ var skPath = new SKPath();
+ skPath.AddPoly(outlinePointList);
+ return skPath;
+ }
+ }
+
+ return new SKPath();
+ }
+
+ ///
+ /// 橡皮擦点擦过程中用到的笔迹信息
+ ///
+ /// 用于中间计算使用
+ class InkInfoForEraserPointPath
+ {
+ public InkInfoForEraserPointPath(SkiaStroke originSkiaStroke)
+ {
+ OriginSkiaStroke = originSkiaStroke;
+
+ SubInkInfoList = new List();
+
+ var subInk = new SubInkInfoForEraserPointPath(new PointListSpan(0, originSkiaStroke.PointList.Count), this);
+ if (originSkiaStroke.Path is { } skPath)
+ {
+ subInk.CacheBounds = skPath.Bounds.ToRect2D();
+ }
+
+ SubInkInfoList.Add(subInk);
+
+ PointList = new Point2D[OriginSkiaStroke.PointList.Count];
+ for (var i = 0; i < OriginSkiaStroke.PointList.Count; i++)
+ {
+ PointList[i] = OriginSkiaStroke.PointList[i].Point;
+ }
+ }
+
+ public SkiaStroke OriginSkiaStroke { get; }
+
+ ///
+ /// 所有实际带的点
+ ///
+ /// 比 结构体小,如此可以提升遍历性能
+ public Point2D[] PointList { get; }
+
+ ///
+ /// 拆分出来的笔迹
+ ///
+ /// 默认会有一条笔迹,就是原始的
+ public List SubInkInfoList { get; }
+
+ ///
+ /// 是否被擦到了
+ ///
+ public bool IsErased
+ {
+ get
+ {
+ if (SubInkInfoList.Count == 1)
+ {
+ var subInk = SubInkInfoList[0];
+ if (subInk.PointListSpan.Start == 0 && subInk.PointListSpan.Length == PointList.Length)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+ }
+
+ ///
+ /// 被橡皮擦拆分的子笔迹信息
+ ///
+ class SubInkInfoForEraserPointPath : IDisposable
+ {
+ public SubInkInfoForEraserPointPath(PointListSpan pointListSpan, InkInfoForEraserPointPath pointPath)
+ {
+ PointListSpan = pointListSpan;
+ PointPath = pointPath;
+ }
+
+ public SKPath? CachePath { get; set; }
+
+ public InkInfoForEraserPointPath PointPath { get; }
+
+ public Rect2D CacheBounds
+ {
+ get
+ {
+ if (_cacheBounds == null)
+ {
+ var span = PointPath.PointList.AsSpan(PointListSpan.Start, PointListSpan.Length);
+ Rect2D bounds = new Rect2D();
+
+ if (span.Length > 0)
+ {
+ bounds = new Rect2D(span[0].X, span[0].Y, 0, 0);
+ }
+
+ for (int i = 1; i < span.Length; i++)
+ {
+ bounds = bounds.Union(span[i]);
+ }
+
+ _cacheBounds = bounds;
+ }
+
+ return _cacheBounds.Value;
+ }
+ set => _cacheBounds = value;
+ }
+
+ private Rect2D? _cacheBounds;
+
+ public PointListSpan PointListSpan { get; }
+
+ public SubInkInfoForEraserPointPath Sub(int start, int length)
+ {
+ return new SubInkInfoForEraserPointPath(new PointListSpan(start, length), PointPath)
+ {
+ _cacheBounds = null
+ };
+ }
+
+ public bool IsDisposed { get; set; }
+
+ public void Dispose()
+ {
+ IsDisposed = true;
+ CachePath?.Dispose();
+ CachePath = null;
+ }
+ }
+
+ readonly record struct PointListSpan(int Start, int Length);
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/PointPathEraserResult.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/PointPathEraserResult.cs
new file mode 100644
index 0000000..bc259b9
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/PointPathEraserResult.cs
@@ -0,0 +1,3 @@
+namespace DotNetCampus.Inking.Erasing;
+
+record PointPathEraserResult(IReadOnlyList ErasingSkiaStrokeList);
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/DelegateEraserViewCreator.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/DelegateEraserViewCreator.cs
new file mode 100644
index 0000000..0d05f04
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/DelegateEraserViewCreator.cs
@@ -0,0 +1,13 @@
+namespace DotNetCampus.Inking.Erasing;
+
+///
+/// 使用委托的方式创建 实例的创建器
+///
+///
+public record DelegateEraserViewCreator(Func Creator) : IEraserViewCreator
+{
+ public IEraserView CreateEraserView()
+ {
+ return Creator();
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/EraserView.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/EraserView.cs
new file mode 100644
index 0000000..a38e943
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/EraserView.cs
@@ -0,0 +1,73 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+
+namespace DotNetCampus.Inking.Erasing;
+
+class EraserView : Control, IEraserView
+{
+ public EraserView()
+ {
+ var rectangleGeometry = new RectangleGeometry(new Rect(0, 0, 48, 72), 3, 3);
+ Path1 = rectangleGeometry;//Geometry.Parse("M0,5.0093855C0,2.24277828,2.2303666,0,5.00443555,0L24.9955644,0C27.7594379,0,30,2.23861485,30,4.99982044L30,17.9121669C30,20.6734914,30,25.1514578,30,27.9102984L30,40.0016889C30,42.7621799,27.7696334,45,24.9955644,45L5.00443555,45C2.24056212,45,0,42.768443,0,39.9906145L0,5.0093855z");
+ //skPaint.Color = new SKColor(0, 0, 0, 0x33);
+ Path1FillBrush = new SolidColorBrush(new Color(0x33, 0, 0, 0));
+
+ var bounds = Path1.Bounds; //.Union(Path2.Bounds);
+ Width = bounds.Width;
+ Height = bounds.Height;
+
+ HorizontalAlignment = HorizontalAlignment.Left;
+ VerticalAlignment = VerticalAlignment.Top;
+ IsHitTestVisible = false;
+
+ var translateTransform = new TranslateTransform();
+ _translateTransform = translateTransform;
+ var scaleTransform = new ScaleTransform();
+ _scaleTransform = scaleTransform;
+ var transformGroup = new TransformGroup();
+ transformGroup.Children.Add(_scaleTransform);
+ transformGroup.Children.Add(_translateTransform);
+ RenderTransform = transformGroup;
+
+ _currentEraserSize = new Size(Width, Height);
+ }
+
+ private readonly TranslateTransform _translateTransform;
+
+ private readonly ScaleTransform _scaleTransform;
+
+ private Geometry Path1 { get; }
+ private IBrush Path1FillBrush { get; }
+
+ //private Geometry Path2 { get; }
+ //private IBrush Path2FillBrush { get; }
+
+ //private IBrush Path3FillBrush { get; }
+
+ private Size _currentEraserSize;
+
+ public void Move(Point position)
+ {
+ _translateTransform.X = position.X - _currentEraserSize.Width / 2;
+ _translateTransform.Y = position.Y - _currentEraserSize.Height / 2;
+ }
+
+ public void SetEraserSize(Size size)
+ {
+ _scaleTransform.ScaleX = size.Width / Width;
+ _scaleTransform.ScaleY = size.Height / Height;
+
+ _currentEraserSize = size;
+ }
+
+ public override void Render(DrawingContext context)
+ {
+ context.DrawGeometry(Path1FillBrush, null, Path1);
+ //skPaint.Color = new SKColor(0xF2, 0xEE, 0xEB, 0xFF);
+ //skCanvas.DrawRoundRect(1, 1, 28, 43, 4, 4, skPaint);
+ //context.DrawRectangle(Path3FillBrush, null, new RoundedRect(new Rect(1, 1, 28, 43), 4));
+ //context.DrawGeometry(Path2FillBrush, null, Path2);
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/IEraserView.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/IEraserView.cs
new file mode 100644
index 0000000..e5c6109
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/IEraserView.cs
@@ -0,0 +1,12 @@
+using Avalonia;
+
+namespace DotNetCampus.Inking.Erasing;
+
+///
+/// 橡皮擦的视图接口
+///
+public interface IEraserView
+{
+ void Move(Point position);
+ void SetEraserSize(Size size);
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/IEraserViewCreator.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/IEraserViewCreator.cs
new file mode 100644
index 0000000..46476ae
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Erasing/Views/IEraserViewCreator.cs
@@ -0,0 +1,6 @@
+namespace DotNetCampus.Inking.Erasing;
+
+public interface IEraserViewCreator
+{
+ IEraserView CreateEraserView();
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Primitive/StylusPointListSpan.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Primitive/StylusPointListSpan.cs
new file mode 100644
index 0000000..0e29f95
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Primitive/StylusPointListSpan.cs
@@ -0,0 +1,20 @@
+namespace DotNetCampus.Inking.Primitive;
+
+public readonly record struct StylusPointListSpan(IReadOnlyList OriginList, int Start, int Length)
+{
+ public IEnumerable GetEnumerable()
+ {
+ return OriginList.Skip(Start).Take(Length);
+ }
+
+ public IReadOnlyList ToReadOnlyList()
+ {
+ var result = new InkStylusPoint[Length];
+ for (int i = 0, listIndex = Start; i < Length; i++, listIndex++)
+ {
+ result[i] = OriginList[listIndex];
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/StrokeRenderers/ISkiaInkStrokeRenderer.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/StrokeRenderers/ISkiaInkStrokeRenderer.cs
new file mode 100644
index 0000000..dbff7af
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/StrokeRenderers/ISkiaInkStrokeRenderer.cs
@@ -0,0 +1,25 @@
+using DotNetCampus.Inking.Primitive;
+
+using SkiaSharp;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DotNetCampus.Inking.StrokeRenderers;
+
+///
+/// 提供给 Skia 系的笔迹渲染器的接口
+///
+public interface ISkiaInkStrokeRenderer
+{
+ ///
+ /// 从给点的点集创建笔迹路径
+ ///
+ ///
+ ///
+ ///
+ SKPath RenderInkToPath(IReadOnlyList pointList, double inkThickness);
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/StrokeRenderers/WpfForSkiaInkStrokeRenderers/SkiaStreamGeometryContext.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/StrokeRenderers/WpfForSkiaInkStrokeRenderers/SkiaStreamGeometryContext.cs
new file mode 100644
index 0000000..1460a41
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/StrokeRenderers/WpfForSkiaInkStrokeRenderers/SkiaStreamGeometryContext.cs
@@ -0,0 +1,65 @@
+using DotNetCampus.Numerics.Geometry;
+
+using SkiaSharp;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using WpfInk;
+
+namespace DotNetCampus.Inking.StrokeRenderers.WpfForSkiaInkStrokeRenderers;
+
+public class SkiaStreamGeometryContext : IStreamGeometryContext
+{
+ public SkiaStreamGeometryContext(SKPath path)
+ {
+ Path = path;
+ path.FillType = SKPathFillType.Winding;
+ }
+
+ public SKPath Path { get; }
+
+ public void BeginFigure(Point2D startPoint, bool isFilled, bool isClosed)
+ {
+ Path.MoveTo(startPoint.ToPoint());
+ }
+
+ public void PolyBezierTo(IList points, bool isStroked, bool isSmoothJoin)
+ {
+ // 传入的 points 必定是 3 的倍数
+
+ for (var i = 0; i < points.Count; i += 3)
+ {
+ var a = points[i];
+ var b = points[i + 1];
+ var c = points[i + 2];
+
+ Path.CubicTo(a.ToPoint(), b.ToPoint(), c.ToPoint());
+ }
+ }
+
+ public void PolyLineTo(IList points, bool isStroked, bool isSmoothJoin)
+ {
+ foreach (var point in points)
+ {
+ Path.LineTo(point.ToPoint());
+ }
+ }
+
+ public void ArcTo(Point2D point, Size2D size, double rotationAngle, bool isLargeArc, bool sweepDirection, bool isStroked,
+ bool isSmoothJoin)
+ {
+ Path.ArcTo((float) size.Width, (float) size.Height, (float) rotationAngle, isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small, sweepDirection ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise, (float) point.X, (float) point.Y);
+ }
+}
+
+file static class Converter
+{
+ public static SKPoint ToPoint(this Point2D point)
+ {
+ return new SKPoint((float) point.X, (float) point.Y);
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/StrokeRenderers/WpfForSkiaInkStrokeRenderers/WpfForSkiaInkStrokeRenderer.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/StrokeRenderers/WpfForSkiaInkStrokeRenderers/WpfForSkiaInkStrokeRenderer.cs
new file mode 100644
index 0000000..8bef687
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/StrokeRenderers/WpfForSkiaInkStrokeRenderers/WpfForSkiaInkStrokeRenderer.cs
@@ -0,0 +1,30 @@
+using DotNetCampus.Inking.Primitive;
+using SkiaSharp;
+using WpfInk;
+
+namespace DotNetCampus.Inking.StrokeRenderers.WpfForSkiaInkStrokeRenderers;
+
+///
+/// 用 WPF 的笔迹算法提供给 Skia 这边的笔迹支持
+///
+public class WpfForSkiaInkStrokeRenderer : ISkiaInkStrokeRenderer
+{
+ public SKPath RenderInkToPath(IReadOnlyList pointList, double inkThickness)
+ {
+ if (pointList.Count == 0)
+ {
+ return new SKPath();
+ }
+
+ var path = new SKPath();
+ var skiaStreamGeometryContext = new SkiaStreamGeometryContext(path);
+
+ InkStrokeRenderer.Render(skiaStreamGeometryContext, new StrokeRendererInfo()
+ {
+ Width = inkThickness / 2,
+ Height = inkThickness / 2,
+ StylusPointCollection = pointList
+ });
+ return path;
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/AvaloniaRectExtension.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/AvaloniaRectExtension.cs
new file mode 100644
index 0000000..c20e897
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/AvaloniaRectExtension.cs
@@ -0,0 +1,17 @@
+using DotNetCampus.Numerics.Geometry;
+using SkiaSharp;
+
+namespace DotNetCampus.Inking.Utils;
+
+static class AvaloniaRectExtension
+{
+ //public static Rect2D ToRect2D(this SKRect rect)
+ //{
+ // return new Rect2D(rect.Left, rect.Top, rect.Width, rect.Height);
+ //}
+
+ public static Rect2D ToRect2D(this Avalonia.Rect rect)
+ {
+ return new Rect2D(rect.Left, rect.Top, rect.Width, rect.Height);
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/PointExtension.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/PointExtension.cs
new file mode 100644
index 0000000..1e8cf12
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/PointExtension.cs
@@ -0,0 +1,16 @@
+using DotNetCampus.Numerics.Geometry;
+
+namespace DotNetCampus.Inking.Utils;
+
+static class PointExtension
+{
+ public static Avalonia.Point ToAvaloniaPoint(this global::DotNetCampus.Numerics.Geometry.Point2D point)
+ {
+ return new Avalonia.Point(point.X, point.Y);
+ }
+
+ public static Point2D ToPoint2D(this Avalonia.Point point)
+ {
+ return new Point2D(point.X, point.Y);
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/RectExtension.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/RectExtension.cs
new file mode 100644
index 0000000..ea5e5e9
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/RectExtension.cs
@@ -0,0 +1,11 @@
+using Avalonia;
+
+namespace DotNetCampus.Inking.Utils;
+
+static class RectExtension
+{
+ public static Rect ExpandLength(this Rect rect, double value)
+ {
+ return new Rect(rect.X - value / 2, rect.Y - value / 2, rect.Width + value, rect.Height + value);
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/SkiaRectExtension.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/SkiaRectExtension.cs
new file mode 100644
index 0000000..4c905bd
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/SkiaRectExtension.cs
@@ -0,0 +1,63 @@
+using DotNetCampus.Numerics;
+using DotNetCampus.Numerics.Geometry;
+
+using SkiaSharp;
+
+namespace DotNetCampus.Inking.Utils;
+
+static class SkiaRectExtension
+{
+ public static SKMatrix ToSkMatrix(this AffineTransformation2D matrix)
+ {
+ return new SKMatrix
+ {
+ ScaleX = (float) matrix.M11,
+ SkewX = (float) matrix.M12,
+ SkewY = (float) matrix.M21,
+ ScaleY = (float) matrix.M22,
+ TransX = (float) matrix.OffsetX,
+ TransY = (float) matrix.OffsetY,
+ Persp0 = 0,
+ Persp1 = 0,
+ Persp2 = 1,
+ };
+ }
+
+ public static SKMatrix ToSkMatrix(this SimilarityTransformation2D transform)
+ {
+ return new SKMatrix
+ {
+ ScaleX = (float) transform.Scaling,
+ SkewX = 0,
+ SkewY = 0,
+ ScaleY = (float) transform.Scaling,
+ TransX = (float) transform.Translation.X,
+ TransY = (float) transform.Translation.Y,
+ Persp0 = 0,
+ Persp1 = 0,
+ Persp2 = 1,
+ };
+ }
+
+ public static AffineTransformation2D ToNumericMatrix(this SKMatrix matrix)
+ {
+ return new AffineTransformation2D(matrix.ScaleX, matrix.SkewX, matrix.SkewY, matrix.ScaleY, matrix.TransX, matrix.TransY);
+ }
+
+ public static SimilarityTransformation2D ToSimilarityTransformation(this SKMatrix matrix)
+ {
+ return matrix.ToNumericMatrix().ToSimilarityTransformation2D();
+ }
+
+ ///
+ /// 将 转换为 ,其中剪切变换将被忽略,非等比缩放将使用最大缩放比进行等比缩放。
+ ///
+ /// 仿射变换。
+ /// 相似变换。
+ public static SimilarityTransformation2D ToSimilarityTransformation2D(this AffineTransformation2D affineTransformation)
+ {
+ var decomposition = affineTransformation.Decompose();
+ var scale = Math.Max(decomposition.Scaling.ScaleX.Abs(), decomposition.Scaling.ScaleY.Abs());
+ return new SimilarityTransformation2D(scale, decomposition.Scaling.ScaleY < 0, decomposition.Rotation, decomposition.Translation);
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/SkiaStrokeRenderSynchronizer.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/SkiaStrokeRenderSynchronizer.cs
new file mode 100644
index 0000000..f22ab1d
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Utils/SkiaStrokeRenderSynchronizer.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DotNetCampus.Inking.Utils;
+
+///
+/// 渲染同步器
+///
+record SkiaStrokeRenderSynchronizer(IReadOnlyList StrokeList, Action OnRender)
+{
+}
+
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/AssemblyInfo.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/AssemblyInfo.cs
new file mode 100644
index 0000000..1b7cbf9
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/AssemblyInfo.cs
@@ -0,0 +1,5 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("DotNetCampus.InkCanvas.SkiaInk")]
+[assembly: InternalsVisibleTo("DotNetCampus.UnoInkCanvas")]
+[assembly: InternalsVisibleTo("DotNetCampus.AvaloniaInkCanvas")]
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/DotNetCampus.InkCanvas.InkCore.csproj b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/DotNetCampus.InkCanvas.InkCore.csproj
new file mode 100644
index 0000000..e31787a
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/DotNetCampus.InkCanvas.InkCore.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net8.0
+ enable
+ enable
+ DotNetCampus.Inking
+ true
+
+
+
+
+
+
+
+
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/DotNetCampus.InkCanvas.InkCore.csproj.DotSettings b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/DotNetCampus.InkCanvas.InkCore.csproj.DotSettings
new file mode 100644
index 0000000..6f053dd
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/DotNetCampus.InkCanvas.InkCore.csproj.DotSettings
@@ -0,0 +1,3 @@
+
+ True
+ True
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Algorithms/DropPointAlgorithm.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Algorithms/DropPointAlgorithm.cs
new file mode 100644
index 0000000..eccb0b8
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Algorithms/DropPointAlgorithm.cs
@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+using DotNetCampus.Inking.Primitive;
+
+namespace DotNetCampus.Inking.Algorithms;
+
+internal static class DropPointAlgorithm
+{
+ ///
+ /// 按照德熙的玄幻算法,决定传入的点是否能丢掉
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static bool CanDropLastPoint(IReadOnlyList pointList, InkStylusPoint currentStylusPoint,
+ int dropPointCount)
+ {
+ if (pointList.Count < 3)
+ {
+ return false;
+ }
+
+ // 已经丢了10个点了,就不继续丢点了
+ if (dropPointCount >= 10)
+ {
+ return false;
+ }
+
+ // 假定要丢掉倒数第一个点,所以上一个点是倒数第二个点
+ var lastPoint = pointList[^2].Point;
+ var currentPoint = currentStylusPoint.Point;
+
+ var lastPointVector = new Vector2((float) lastPoint.X, (float) lastPoint.Y);
+ var currentPointVector = new Vector2((float) currentPoint.X, (float) currentPoint.Y);
+
+ var lineVector = currentPointVector - lastPointVector;
+ var lineLength = lineVector.Length();
+
+ // 如果移动距离比较长,则不丢点
+ if (lineLength > 10)
+ {
+ return false;
+ }
+
+ var last2Point = pointList[^3].Point;
+ var line2Vector = lastPointVector - new Vector2((float) last2Point.X, (float) last2Point.Y);
+ var line2Length = line2Vector.Length();
+ var vector2 = currentPointVector - lastPointVector;
+ var distance2 = MathF.Abs(line2Vector.X * vector2.Y - line2Vector.Y * vector2.X) / line2Length;
+ if (distance2 > 2)
+ {
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Context_/InkId.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Context_/InkId.cs
new file mode 100644
index 0000000..15c8b04
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Context_/InkId.cs
@@ -0,0 +1,11 @@
+namespace DotNetCampus.Inking;
+
+public readonly partial record struct InkId(int Value)
+{
+ public static InkId NewId() => new InkId(_nextId++);
+
+ private static int _nextId = 0;
+
+ public override string ToString()
+ => $"InkId={Value}";
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Diagnostics/AverageCounter.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Diagnostics/AverageCounter.cs
new file mode 100644
index 0000000..c786b4f
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Diagnostics/AverageCounter.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DotNetCampus.Inking.Diagnostics;
+
+class AverageCounter
+{
+ public AverageCounter(string name, int averageMaxCount = 100) : this(averageMaxCount, averageTime => StaticDebugLogger.WriteLine($"{name} 耗时: {averageTime}"))
+ {
+ }
+
+ public AverageCounter(int averageMaxCount, AverageCounterRecordHandler handler)
+ {
+ _averageMaxCount = averageMaxCount;
+ _handler = handler;
+
+#if DEBUG
+ Enable = true;
+#endif
+ }
+
+ ///
+ /// 是否可用,用于方便一口气关闭
+ ///
+ public bool Enable { get; set; }
+
+ public void Start()
+ {
+ _stopwatch.Restart();
+ }
+
+ public void Stop()
+ {
+ _stopwatch.Stop();
+ _totalTime += _stopwatch.Elapsed.TotalMilliseconds;
+ _count++;
+
+ if (_count >= _averageMaxCount)
+ {
+ _handler(_totalTime / _count);
+ _totalTime = 0;
+ _count = 0;
+ }
+ }
+
+ private readonly Stopwatch _stopwatch = new Stopwatch();
+ private double _totalTime;
+ private int _count;
+ private readonly int _averageMaxCount;
+ private readonly AverageCounterRecordHandler _handler;
+}
+
+delegate void AverageCounterRecordHandler(double averageTime);
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Diagnostics/StaticDebugLogger.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Diagnostics/StaticDebugLogger.cs
new file mode 100644
index 0000000..4ee1e2d
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Diagnostics/StaticDebugLogger.cs
@@ -0,0 +1,20 @@
+using System.Diagnostics;
+using DotNetCampus.Logging;
+
+namespace DotNetCampus.Inking.Diagnostics;
+
+static class StaticDebugLogger
+{
+ [Conditional("False")]
+ public static void WriteLine(string message)
+ {
+ //if (!message.Contains("X11DeviceInputManager"))
+ //{
+ // return;
+ //}
+
+ Log.Debug($"[InkCore] {message}");
+
+ //Console.WriteLine(message);
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Diagnostics/StepCounter.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Diagnostics/StepCounter.cs
new file mode 100644
index 0000000..2702897
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Diagnostics/StepCounter.cs
@@ -0,0 +1,118 @@
+using System.Diagnostics;
+using System.Text;
+
+namespace DotNetCampus.Inking.Diagnostics;
+
+///
+/// 线性步骤记录器
+///
+class StepCounter
+{
+ ///
+ /// 开始
+ ///
+ /// 开始和记录分离,开始不一定是某个步骤。这样业务方修改开始对应的步骤时,可以能够更好的被约束,明确一个开始的时机
+ public void Start()
+ {
+ Stopwatch.Restart();
+ IsStart = true;
+ }
+
+ public void Restart()
+ {
+ IsStart = true;
+ StepDictionary.Clear();
+ Stopwatch.Restart();
+ }
+
+ public Stopwatch Stopwatch => _stopwatch ??= new Stopwatch();
+ private Stopwatch? _stopwatch;
+
+ ///
+ /// 记录某个步骤。默认就是一个步骤将会延续到下个步骤,两个步骤之间的耗时就是步骤耗时
+ /// 实在不行,那你就加上 “Xx开始” 和 “Xx结束”好了
+ ///
+ ///
+ public void Record(string step)
+ {
+ if (!IsStart)
+ {
+ return;
+ }
+
+ Stopwatch.Stop();
+ StepDictionary[step] = Stopwatch.ElapsedTicks;
+ Stopwatch.Restart();
+ }
+
+ public void OutputToConsole()
+ {
+ if (!IsStart)
+ {
+ return;
+ }
+ Console.WriteLine(BuildStepResult());
+ }
+
+ ///
+ /// 进行耗时对比,用于对比两个模块或者两个版本的各个步骤的耗时差
+ ///
+ ///
+ public void CompareToConsole(StepCounter other)
+ {
+ if (!IsStart)
+ {
+ return;
+ }
+ Console.WriteLine(Compare(other));
+ }
+
+ public string Compare(StepCounter other)
+ {
+ if (!IsStart)
+ {
+ return string.Empty;
+ }
+
+ var stringBuilder = new StringBuilder();
+ foreach (var (step, tick) in StepDictionary)
+ {
+ if (other.StepDictionary.TryGetValue(step, out var otherTick))
+ {
+ var sign = tick > otherTick ? "+" : "";
+ stringBuilder.AppendLine($"{step} {TickToMillisecond(tick):0.000}ms {TickToMillisecond(otherTick):0.000}ms {sign}{TickToMillisecond(tick - otherTick):0.000}ms");
+ }
+ else
+ {
+ stringBuilder.AppendLine($"{step} {tick * 1000d / Stopwatch.Frequency}ms");
+ }
+ }
+ return stringBuilder.ToString();
+ }
+
+ public string BuildStepResult()
+ {
+ if (!IsStart)
+ {
+ return string.Empty;
+ }
+
+ var stringBuilder = new StringBuilder();
+ foreach (var (step, tick) in StepDictionary)
+ {
+ stringBuilder.AppendLine($"{step} {TickToMillisecond(tick)}ms");
+ }
+ return stringBuilder.ToString();
+ }
+
+ public Dictionary StepDictionary => _stepDictionary ??= new Dictionary();
+ private Dictionary? _stepDictionary;
+
+ ///
+ /// 是否开始,如果没有开始则啥都不做,用于性能优化,方便一次性注释决定是否测试性能
+ ///
+ public bool IsStart { get; private set; }
+
+ private const double SecondToMillisecond = 1000d;
+ private static double TickToMillisecond(long tick) => tick * SecondToMillisecond / Stopwatch.Frequency;
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/IInkingInputProcessor.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/IInkingInputProcessor.cs
new file mode 100644
index 0000000..ad4962b
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/IInkingInputProcessor.cs
@@ -0,0 +1,28 @@
+namespace DotNetCampus.Inking.Interactives;
+
+///
+/// 输入处理者
+///
+interface IInkingInputProcessor
+{
+ ///
+ /// 是否有效,是否接受输入
+ ///
+ bool Enable { get; }
+
+ InkingInputProcessorSettings InputProcessorSettings => InkingInputProcessorSettings.Default;
+
+ void InputStart();
+
+ void Down(InkingModeInputArgs args);
+
+ void Move(InkingModeInputArgs args);
+
+ void Hover(InkingModeInputArgs args);
+
+ void Up(InkingModeInputArgs args);
+
+ void Leave();
+
+ void InputComplete();
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/IInkingModeInputDispatcherSensitive.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/IInkingModeInputDispatcherSensitive.cs
new file mode 100644
index 0000000..ff1aa2e
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/IInkingModeInputDispatcherSensitive.cs
@@ -0,0 +1,12 @@
+namespace DotNetCampus.Inking.Interactives;
+
+///
+/// 表示对输入调度器敏感,将被注入
+///
+interface IInkingModeInputDispatcherSensitive
+{
+ ///
+ /// 输入调度器 此属性将由框架层注入值
+ ///
+ InkingModeInputDispatcher ModeInputDispatcher { set; get; }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/InkingInputProcessorSettings.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/InkingInputProcessorSettings.cs
new file mode 100644
index 0000000..1277ace
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/InkingInputProcessorSettings.cs
@@ -0,0 +1,14 @@
+namespace DotNetCampus.Inking.Interactives;
+
+record InkingInputProcessorSettings
+{
+ // 不好实现,存在漏洞是首次收到 Move 的情况,此时不仅需要补 Down 还需要补 Start 的情况
+ /////
+ ///// 对于丢失了 Down 的触摸,是否启用。如启用,则会自动补 Down 事件。默认 false 即丢点
+ /////
+ //public bool EnableLostDownTouch { init; get; } = false;
+
+ public bool EnableMultiTouch { init; get; } = true;
+
+ public static readonly InkingInputProcessorSettings Default = new InkingInputProcessorSettings();
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/InkingModeInputArgs.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/InkingModeInputArgs.cs
new file mode 100644
index 0000000..a79a2c0
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/InkingModeInputArgs.cs
@@ -0,0 +1,19 @@
+using DotNetCampus.Inking.Primitive;
+using DotNetCampus.Numerics.Geometry;
+
+namespace DotNetCampus.Inking.Interactives;
+
+public readonly record struct InkingModeInputArgs(int Id, InkStylusPoint StylusPoint, ulong Timestamp)
+{
+ public Point2D Position => StylusPoint.Point;
+
+ ///
+ /// 是否来自鼠标的输入
+ ///
+ public bool IsMouse { init; get; }
+
+ ///
+ /// 被合并的其他历史的触摸点。可能为空
+ ///
+ public IReadOnlyList? StylusPointList { init; get; }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/InkingModeInputDispatcher.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/InkingModeInputDispatcher.cs
new file mode 100644
index 0000000..742f9fd
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Interactives/InkingModeInputDispatcher.cs
@@ -0,0 +1,197 @@
+using System.Diagnostics;
+using DotNetCampus.Inking.Diagnostics;
+
+namespace DotNetCampus.Inking.Interactives;
+
+///
+/// 输入调度器
+///
+class InkingModeInputDispatcher
+{
+ private HashSet CurrentInputIdHashSet { get; } = new HashSet();
+
+ ///
+ /// 首个输入点
+ ///
+ public int MainInputId { private set; get; }
+
+ ///
+ /// 是否输入开始了
+ ///
+ public bool IsInputStart { private set; get; }
+
+ ///
+ /// 距离输入过去多久,仅在 为 true 时,才有意义
+ ///
+ public TimeSpan InputDuring => _inputDuringStopwatch.Elapsed;
+
+ private readonly Stopwatch _inputDuringStopwatch = new Stopwatch();
+
+ public void Down(in InkingModeInputArgs args)
+ {
+ CurrentInputIdHashSet.Add(args.Id);
+
+ if (CurrentInputIdHashSet.Count == 1)
+ {
+ MainInputId = args.Id;
+ ProcessInputStart();
+ }
+
+ ProcessDown(in args);
+ }
+
+ private void ProcessInputStart()
+ {
+ IsInputStart = true;
+ _inputDuringStopwatch.Restart();
+
+ foreach (var inputProcessor in InputProcessors)
+ {
+ if (inputProcessor.Enable)
+ {
+ inputProcessor.InputStart();
+ }
+ }
+ }
+
+ private void ProcessDown(in InkingModeInputArgs args)
+ {
+ foreach (var inputProcessor in InputProcessors)
+ {
+ if (inputProcessor.Enable)
+ {
+ inputProcessor.Down(args);
+ }
+ }
+ }
+
+ public void Move(in InkingModeInputArgs args)
+ {
+ if (CurrentInputIdHashSet.Contains(args.Id))
+ {
+ foreach (var inputProcessor in InputProcessors)
+ {
+ if (inputProcessor.Enable)
+ {
+ if (inputProcessor.InputProcessorSettings.EnableMultiTouch || MainInputId == args.Id)
+ {
+ inputProcessor.Move(args);
+ }
+ }
+ }
+ }
+ else
+ {
+ if (args.IsMouse)
+ {
+ foreach (var inputProcessor in InputProcessors)
+ {
+ if (inputProcessor.Enable)
+ {
+ inputProcessor.Hover(args);
+ }
+ }
+ }
+ else
+ {
+ // 非鼠标没有 Hover 效果
+ // 如果是在 IsInputStart=false 时,代表触摸离开之后,收到离开之后的消息
+ // 对应的问题记录:手势橡皮擦进入工具条时,先触发 Leave 里面,符合预期的进行结束手势橡皮擦。然而后续居然又继续收到 Move 事件,导致判断橡皮擦逻辑工作,再次错误进入了手势橡皮擦模式
+ StaticDebugLogger.WriteLine($"[{nameof(InkingModeInputDispatcher)}] Lost Move IsInputStart={IsInputStart} Id={args.Id}");
+ }
+ }
+ }
+
+ public void Up(InkingModeInputArgs args)
+ {
+ if (CurrentInputIdHashSet.Remove(args.Id))
+ {
+ if (args.Id == MainInputId)
+ {
+ StaticDebugLogger.WriteLine($"[{nameof(InkingModeInputDispatcher)}] MainIdUp MainId={MainInputId}");
+ }
+
+ foreach (var inputProcessor in InputProcessors)
+ {
+ if (inputProcessor.Enable)
+ {
+ if (inputProcessor.InputProcessorSettings.EnableMultiTouch || MainInputId == args.Id)
+ {
+ inputProcessor.Up(args);
+ }
+ }
+ }
+
+ if (CurrentInputIdHashSet.Count == 0)
+ {
+ ProcessInputComplete();
+ }
+ }
+ else
+ {
+ // 啥都不能做
+ }
+ }
+
+ private void ProcessInputComplete()
+ {
+ foreach (var inputProcessor in InputProcessors)
+ {
+ if (inputProcessor.Enable)
+ {
+ inputProcessor.InputComplete();
+ }
+ }
+ IsInputStart = false;
+ _inputDuringStopwatch.Stop();
+ }
+
+ ///
+ /// 输入被其他拿走了,比如鼠标移动到窗口外抬起
+ ///
+ public void Leave()
+ {
+ StaticDebugLogger.WriteLine($"{nameof(InkingModeInputDispatcher)} Leave");
+
+ foreach (var inputProcessor in InputProcessors)
+ {
+ if (inputProcessor.Enable)
+ {
+ inputProcessor.Leave();
+ }
+ }
+
+ CurrentInputIdHashSet.Clear();
+ IsInputStart = false;
+ _inputDuringStopwatch.Stop();
+ }
+
+ ///
+ /// 加上输入处理者,有输入时自然执行
+ ///
+ ///
+ public void AddInputProcessor(IInkingInputProcessor inkingInputProcessor)
+ {
+ InputProcessors.Add(inkingInputProcessor);
+ if (inkingInputProcessor is IInkingModeInputDispatcherSensitive modeInputDispatcherSensitive)
+ {
+ modeInputDispatcherSensitive.ModeInputDispatcher = this;
+ }
+ }
+
+ private List InputProcessors { get; } = new List();
+
+ //public bool Enable => true;
+
+ ///
+ /// 某个触摸 Id 是否存在。不存在则代表被抬起
+ ///
+ ///
+ ///
+ public bool ContainsDeviceId(int deviceId) => CurrentInputIdHashSet.Contains(deviceId);
+
+ ///
+ /// 当前有多少手指按下
+ ///
+ public int CurrentDeviceCount => CurrentInputIdHashSet.Count;
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Primitive/InkStylusPoint.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Primitive/InkStylusPoint.cs
new file mode 100644
index 0000000..b1f7e4b
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Primitive/InkStylusPoint.cs
@@ -0,0 +1,29 @@
+using DotNetCampus.Numerics.Geometry;
+
+namespace DotNetCampus.Inking.Primitive;
+
+public readonly record struct InkStylusPoint
+{
+ public InkStylusPoint(Point2D point, float pressure = DefaultPressure)
+ {
+ Point = point;
+ Pressure = pressure;
+ }
+
+ public InkStylusPoint(double x, double y, float pressure = DefaultPressure) : this(new Point2D(x, y), pressure)
+ {
+ }
+
+ public double X => Point.X;
+ public double Y => Point.Y;
+
+ public Point2D Point { init; get; }
+ public float Pressure { init; get; }
+
+ //public bool IsPressureEnable { init; get; }
+
+ public double? Width { init; get; }
+ public double? Height { init; get; }
+
+ public const float DefaultPressure = 0.5f;
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Primitive/RotatedRect.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Primitive/RotatedRect.cs
new file mode 100644
index 0000000..aceceab
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Primitive/RotatedRect.cs
@@ -0,0 +1,293 @@
+using DotNetCampus.Numerics;
+using DotNetCampus.Numerics.Geometry;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace DotNetCampus.Inking.Primitive;
+
+///
+/// 带有旋转的矩形。
+///
+public readonly record struct RotatedRect : ISimilarityTransformable2D
+{
+ ///
+ /// 创建一个带有旋转的矩形。
+ ///
+ /// 矩形所在的位置。UI 上表示为未旋转前矩形的左上角。
+ /// 矩形的大小。
+ /// 矩形的旋转角度。旋转的中心点为 。
+ public RotatedRect(Point2D location, Size2D size, AngularMeasure rotate)
+ {
+ Location = location;
+ Size = size;
+ Rotate = rotate;
+ }
+
+ ///
+ /// 矩形所在的位置。UI 上表示为未旋转前矩形的左上角。
+ ///
+ public Point2D Location { get; }
+
+ ///
+ /// 矩形的大小。
+ ///
+ public Size2D Size { get; }
+
+ ///
+ /// 矩形的旋转角度。旋转的中心点为 。
+ ///
+ public AngularMeasure Rotate { get; }
+
+ ///
+ public RotatedRect Transform(SimilarityTransformation2D transformation)
+ {
+ var newLocation = transformation.Transform(Location);
+ var newRotate = Rotate + transformation.Rotation;
+ if (transformation.IsYScaleNegative)
+ {
+ newLocation -= transformation.Scaling * Size.Height * newRotate.UnitVector.NormalVector;
+ }
+
+ var newSize = new Size2D(Size.Width * transformation.Scaling, Size.Height * transformation.Scaling);
+ return new RotatedRect(newLocation, newSize, newRotate.Normalized);
+ }
+
+ ///
+ /// 判断指定的点是否在矩形内。这里考虑了矩形的旋转。
+ ///
+ /// 要判断的点。
+ ///
+ public bool Contains(Point2D point)
+ {
+ var transformation = AffineTransformation2D.Identity.RotateAt(-Rotate, Location);
+ var transformedPoint = transformation.Transform(point);
+ return transformedPoint.X >= Location.X && transformedPoint.X <= Location.X + Size.Width &&
+ transformedPoint.Y >= Location.Y && transformedPoint.Y <= Location.Y + Size.Height;
+ }
+
+ ///
+ /// 过滤掉不在矩形内的点。这里考虑了矩形的旋转。
+ ///
+ ///
+ ///
+ public IEnumerable FilterContained(IEnumerable points)
+ {
+ var transformation = AffineTransformation2D.Identity.RotateAt(-Rotate, Location).Translate(-Location.ToVector());
+ foreach (var point in points)
+ {
+ var transformedPoint = transformation.Transform(point);
+ if (transformedPoint.X >= 0 && transformedPoint.X <= Size.Width && transformedPoint.Y >= 0 && transformedPoint.Y <= Size.Height)
+ {
+ yield return point;
+ }
+ }
+ }
+
+ ///
+ /// 获取矩形的包围盒。这里考虑了矩形的旋转。
+ ///
+ ///
+ public BoundingBox2D GetBoundingBox()
+ {
+ var transformation = AffineTransformation2D.Identity.RotateAt(Rotate, Location);
+ var points = new[]
+ {
+ Location,
+ Location + new Vector2D(Size.Width, 0),
+ Location + new Vector2D(Size.Width, Size.Height),
+ Location + new Vector2D(0, Size.Height),
+ };
+
+ for (var index = 0; index < points.Length; index++)
+ {
+ points[index] = transformation.Transform(points[index]);
+ }
+
+ return BoundingBox2D.Create(points);
+ }
+
+ public RotatedRect FlipAtBottom()
+ {
+ var widthUnitVector = Rotate.UnitVector;
+ var heightUnitVector = Rotate.UnitVector.NormalVector;
+ var newLocation = Location + Size.Width * widthUnitVector + 2 * Size.Height * heightUnitVector;
+ return new RotatedRect(newLocation, Size, (Rotate + AngularMeasure.Pi).Normalized);
+ }
+
+ ///
+ /// 是否与另一个带旋转的矩形相交。
+ ///
+ /// 另一个带旋转的矩形。
+ /// 是否相交。
+ public bool Intersects(RotatedRect other)
+ {
+ var widthUnitVector1 = Rotate.UnitVector;
+ var heightUnitVector1 = Rotate.UnitVector.NormalVector;
+ var widthUnitVector2 = other.Rotate.UnitVector;
+ var heightUnitVector2 = other.Rotate.UnitVector.NormalVector;
+ var axisUnitVectors = new[]
+ {
+ widthUnitVector1,
+ heightUnitVector1,
+ widthUnitVector2,
+ heightUnitVector2,
+ };
+
+ var vectors1 = new[]
+ {
+ Location.ToVector(),
+ Location.ToVector() + Size.Width * widthUnitVector1,
+ Location.ToVector() + Size.Width * widthUnitVector1 + Size.Height * heightUnitVector1,
+ Location.ToVector() + Size.Height * heightUnitVector1,
+ };
+ var vectors2 = new[]
+ {
+ other.Location.ToVector(),
+ other.Location.ToVector() + other.Size.Width * widthUnitVector2,
+ other.Location.ToVector() + other.Size.Width * widthUnitVector2 + other.Size.Height * heightUnitVector2,
+ other.Location.ToVector() + other.Size.Height * heightUnitVector2,
+ };
+
+ foreach (var axisUnitVector in axisUnitVectors)
+ {
+ var min1 = double.MaxValue;
+ var max1 = double.MinValue;
+ var min2 = double.MaxValue;
+ var max2 = double.MinValue;
+ foreach (var vector in vectors1)
+ {
+ var projection = axisUnitVector.Dot(vector);
+ min1 = Math.Min(min1, projection);
+ max1 = Math.Max(max1, projection);
+ }
+
+ foreach (var vector in vectors2)
+ {
+ var projection = axisUnitVector.Dot(vector);
+ min2 = Math.Min(min2, projection);
+ max2 = Math.Max(max2, projection);
+ }
+
+ if (max1 < min2 || max2 < min1)
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ ///
+ /// 是否与指定的点集相交。
+ ///
+ /// 指定的点集。
+ /// 点集是否视为折线。
+ /// 是否相交。
+ public bool Intersects(IEnumerable points, bool isPolyline)
+ {
+ var transformation = AffineTransformation2D.Identity.RotateAt(-Rotate, Location).Translate(-Location.ToVector());
+
+ if (isPolyline)
+ {
+ Point2D? lastPoint = null;
+ foreach (var point in points)
+ {
+ var currentPoint = transformation.Transform(point);
+
+ if (lastPoint is not { } lastPointValue)
+ {
+ if (currentPoint.X >= 0 && currentPoint.X <= Size.Width && currentPoint.Y >= 0 && currentPoint.Y <= Size.Height)
+ {
+ return true;
+ }
+
+ lastPoint = point;
+ continue;
+ }
+
+ var vector = currentPoint - lastPointValue;
+ if (vector.LengthSquared.IsAlmostZero())
+ {
+ continue;
+ }
+
+ if (!vector.X.IsAlmostEqual(0))
+ {
+ var k = vector.Y / vector.X;
+ var y = currentPoint.Y - currentPoint.X * k;
+ if (y >= 0 && y <= Size.Height)
+ {
+ return true;
+ }
+
+ y = currentPoint.Y - (currentPoint.X - Size.Width) * k;
+ if (y >= 0 && y <= Size.Height)
+ {
+ return true;
+ }
+ }
+
+ if (!vector.Y.IsAlmostEqual(0))
+ {
+ // 斜率的倒数
+ var rk = vector.X / vector.Y;
+ var x = currentPoint.X - currentPoint.Y * rk;
+ if (x >= 0 && x <= Size.Width)
+ {
+ return true;
+ }
+
+ x = currentPoint.X - (currentPoint.Y - Size.Height) * rk;
+ if (x >= 0 && x <= Size.Width)
+ {
+ return true;
+ }
+ }
+
+ lastPoint = point;
+ }
+ }
+ else
+ {
+ foreach (var point in points)
+ {
+ var transformedPoint = transformation.Transform(point);
+ if (transformedPoint.X >= 0 && transformedPoint.X <= Size.Width && transformedPoint.Y >= 0 && transformedPoint.Y <= Size.Height)
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// 在矩形的四个边上分别向外扩张形成新的矩形。
+ ///
+ /// 宽度上的扩张量。
+ /// 高度上的扩张量。
+ /// 扩张后的矩形。
+ public RotatedRect Inflate(double widthAmount, double heightAmount)
+ {
+ var widthUnitVector = Rotate.UnitVector;
+ var heightUnitVector = Rotate.UnitVector.NormalVector;
+ var newLocation = Location - widthAmount * widthUnitVector - heightAmount * heightUnitVector;
+ var newSize = new Size2D(Size.Width + 2 * widthAmount, Size.Height + 2 * heightAmount);
+ return new RotatedRect(newLocation, newSize, Rotate);
+ }
+
+ ///
+ /// 在矩形的四个边上分别向外扩张形成新的矩形。
+ ///
+ /// 扩张量。
+ /// 扩张后的矩形。
+ public RotatedRect Inflate(double amount)
+ {
+ return Inflate(amount, amount);
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Utils/InkingFixedQueue.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Utils/InkingFixedQueue.cs
new file mode 100644
index 0000000..249fd9f
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Utils/InkingFixedQueue.cs
@@ -0,0 +1,148 @@
+using System.Collections;
+using System.ComponentModel;
+
+namespace DotNetCampus.Inking.Utils;
+
+///
+/// 带最大数量的队列,超过最大数量将会自动将队头元素出队丢弃
+///
+///
+class InkingFixedQueue : ICollection, IEnumerable
+{
+ private readonly Queue _innerQueue = new Queue();
+
+ ///
+ /// 创建带最大数量的队列
+ ///
+ ///
+ public InkingFixedQueue(int maxCount)
+ {
+ MaxCount = maxCount;
+ }
+
+ ///
+ /// 队列可以使用的最大元素数量
+ ///
+ public int MaxCount { get; private set; }
+
+ #region Queue相关的成员
+ ///
+ /// Gets the enumerator.
+ ///
+ ///
+ public IEnumerator GetEnumerator()
+ {
+ return _innerQueue.GetEnumerator();
+ }
+
+ ///
+ /// 返回一个循环访问集合的枚举器。
+ ///
+ ///
+ /// 可用于循环访问集合的 对象。
+ ///
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _innerQueue.GetEnumerator();
+ }
+
+ ///
+ /// 从特定的 索引处开始,将 的元素复制到一个 中。
+ ///
+ /// 作为从 复制的元素的目标位置的一维 。 必须具有从零开始的索引。
+ /// 中从零开始的索引,将在此处开始复制。
+ public void CopyTo(Array array, int index)
+ {
+ ((ICollection) _innerQueue).CopyTo(array, index);
+ }
+
+ ///
+ /// Copies to.
+ ///
+ /// The array.
+ /// The index.
+ public void CopyTo(T[] array, int index)
+ {
+ _innerQueue.CopyTo(array, index);
+ }
+
+ ///
+ /// 获取 中包含的元素数。
+ ///
+ ///
+ /// 中包含的元素数。
+ public int Count { get { return _innerQueue.Count; } }
+ ///
+ /// 获取一个可用于同步对 的访问的对象。
+ ///
+ /// 可用于同步对 的访问的对象。
+ public object SyncRoot { get { return ((ICollection) _innerQueue).SyncRoot; } }
+
+ ///
+ /// 获取一个值,该值指示是否同步对 的访问(线程安全)。
+ ///
+ /// 如果对 的访问是同步的(线程安全),则为 true;否则为 false。
+ public bool IsSynchronized => false;// 因为包装不是线程安全的,如 Enqueue 等方法,所以整个类是线程不安全的
+ #endregion
+
+ ///
+ /// 将对象添加到队列结尾处
+ ///
+ ///
+ public void Enqueue(T item)
+ {
+ if (_innerQueue.Count > MaxCount)
+ {
+ throw new InvalidOperationException("集合中的元素已超过最大限定值。");
+ }
+ if (_innerQueue.Count == MaxCount)
+ {
+ _innerQueue.Dequeue();
+ }
+ _innerQueue.Enqueue(item);
+ }
+
+ ///
+ /// 移除并返回队列开始处元素
+ ///
+ /// 一个没有被使用的元素,请随意传入,这是设计问题,但为了兼容性,暂时保存
+ ///
+ [Obsolete("请使用不带参数的 Dequeue 方法代替,这个方法传入的参数没有被使用")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ public T Dequeue(T item)
+ {
+ return _innerQueue.Dequeue();
+ }
+
+ ///
+ /// 移除并返回队列开始处元素
+ ///
+ public T Dequeue() => _innerQueue.Dequeue();
+
+ ///
+ /// 返回队列开始处的对象但不将这个对象移除
+ ///
+ ///
+ public T Peek()
+ {
+ return _innerQueue.Peek();
+ }
+
+ ///
+ /// 确定某元素是否在队列存在
+ ///
+ ///
+ ///
+ public bool Contains(T item)
+ {
+ return _innerQueue.Contains(item);
+ }
+
+ ///
+ /// 清空队列
+ ///
+ public void Clear()
+ {
+ _innerQueue.Clear();
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/ReferenceCode/Mathematics/SpatialGeometry/Rect2D.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/ReferenceCode/Mathematics/SpatialGeometry/Rect2D.cs
new file mode 100644
index 0000000..6d4f27e
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/ReferenceCode/Mathematics/SpatialGeometry/Rect2D.cs
@@ -0,0 +1,121 @@
+using System.Drawing;
+using DotNetCampus.Numerics.Geometry;
+using Rect = DotNetCampus.Numerics.Geometry.Rect2D;
+
+namespace DotNetCampus.Numerics.Geometry;
+
+#if HAS_UNO
+public
+#endif
+readonly record struct Rect2D(Point2D Location, Size2D Size)
+{
+ public Rect2D(double x, double y, double width, double height) : this(new Point2D(x, y), new Size2D(width, height))
+ {
+ }
+
+ public double X => Location.X;
+ public double Y => Location.Y;
+
+ public double Width => Size.Width;
+ public double Height => Size.Height;
+
+ public double Left => X;
+ public double Top => Y;
+ public double Right => X + Width;
+ public double Bottom => Y + Height;
+
+ public bool IsEmpty => (Width <= 0) || (Height <= 0);
+
+ public static Rect2D Zero => default;
+
+ public static Rect2D FromLTRB(double left, double top, double right, double bottom)
+ {
+ return new Rect2D(left, top, right - left, bottom - top);
+ }
+
+ public bool Contains(Rect2D rect)
+ {
+ return X <= rect.X && Right >= rect.Right && Y <= rect.Y && Bottom >= rect.Bottom;
+ }
+
+ public bool Contains(Point2D pt)
+ {
+ return Contains(pt.X, pt.Y);
+ }
+
+ public bool Contains(double x, double y)
+ {
+ return (x >= Left) && (x < Right) && (y >= Top) && (y < Bottom);
+ }
+
+ public bool IntersectsWith(Rect2D r)
+ {
+ return !((Left >= r.Right) || (Right <= r.Left) || (Top >= r.Bottom) || (Bottom <= r.Top));
+ }
+
+ public Rect2D Union(Rect2D r)
+ {
+ return Union(this, r);
+ }
+
+ public Rect Union(Point2D pt)
+ {
+ var left = Math.Min(Left, pt.X);
+ var top = Math.Min(Top, pt.Y);
+ var right = Math.Max(Right, pt.X);
+ var bottom = Math.Max(Bottom, pt.Y);
+ return FromLTRB(left, top, right, bottom);
+ }
+
+ public static Rect2D Union(Rect2D r1, Rect2D r2)
+ {
+ return FromLTRB(Math.Min(r1.Left, r2.Left), Math.Min(r1.Top, r2.Top), Math.Max(r1.Right, r2.Right), Math.Max(r1.Bottom, r2.Bottom));
+ }
+
+ public Rect2D Intersect(Rect2D r)
+ {
+ return Intersect(this, r);
+ }
+
+ public static Rect2D Intersect(Rect2D r1, Rect2D r2)
+ {
+ double x = Math.Max(r1.X, r2.X);
+ double y = Math.Max(r1.Y, r2.Y);
+ double width = Math.Min(r1.Right, r2.Right) - x;
+ double height = Math.Min(r1.Bottom, r2.Bottom) - y;
+
+ if (width < 0 || height < 0)
+ {
+ return Zero;
+ }
+ return new Rect2D(x, y, width, height);
+ }
+
+ public Rect2D Inflate(Size2D sz)
+ {
+ return Inflate(sz.Width, sz.Height);
+ }
+
+ public Rect2D Inflate(double width, double height)
+ {
+ return new Rect2D(X - width, Y - height, Width + width, Height + height);
+ }
+
+ public Rect2D Offset(double dx, double dy)
+ {
+ return this with
+ {
+ Location = new Point2D(X + dx, Y + dy)
+ };
+ }
+
+ public Rect2D Offset(Point2D dr)
+ {
+ return Offset(dr.X, dr.Y);
+ }
+
+ public Rect2D Round()
+ {
+ return new Rect2D(Math.Round(X), Math.Round(Y), Math.Round(Width), Math.Round(Height));
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/SimpleInkRender.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/SimpleInkRender.cs
new file mode 100644
index 0000000..16c1b74
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/SimpleInkRender.cs
@@ -0,0 +1,159 @@
+using System.Numerics;
+using DotNetCampus.Inking.Primitive;
+using Point = DotNetCampus.Numerics.Geometry.Point2D;
+
+namespace DotNetCampus.Inking
+{
+ ///
+ /// 特别简单的笔迹渲染器。
+ ///
+ internal static class SimpleInkRender
+ {
+ private static readonly Matrix3x2 RotationPiDiv8 = Matrix3x2.CreateRotation(MathF.PI / 8);
+ private static readonly Matrix3x2 RotationPiDiv4 = Matrix3x2.CreateRotation(MathF.PI / 4);
+ private static readonly Matrix3x2 Rotation3PiDiv8 = Matrix3x2.CreateRotation(3 * MathF.PI / 8);
+
+ public static Point[] GetOutlinePointList(IReadOnlyList pointList, double inkSize)
+ {
+ if (pointList.Count < 2)
+ {
+ throw new ArgumentException("小于两个点的无法应用算法");
+ }
+
+ var outlinePointList1 = new List(pointList.Count * 2);
+ var outlinePointList2 = new List(pointList.Count * 2);
+
+ for (var i = 0; i < pointList.Count; i++)
+ {
+ // 笔迹粗细的一半,一边用一半,合起来就是笔迹粗细了
+ var halfThickness = (float) inkSize / 2;
+
+ // 压感这里是直接乘法而已
+ halfThickness *= pointList[i].Pressure;
+ // 不能让笔迹粗细太小
+ halfThickness = MathF.Max(0.01f, halfThickness);
+
+ if (i == 0 || pointList[i].Point == pointList[i - 1].Point)
+ {
+ if (i == pointList.Count - 1 || pointList[i].Point == pointList[i + 1].Point)
+ {
+ continue;
+ }
+
+ var direction = Vector2.Multiply(halfThickness, Vector2.Normalize(new Vector2((float) pointList[i + 1].Point.X - (float) pointList[i].Point.X, (float) pointList[i + 1].Point.Y - (float) pointList[i].Point.Y)));
+
+ var point1 = new Point(pointList[i].Point.X - direction.Y, pointList[i].Point.Y + direction.X);
+ var point2 = new Point(pointList[i].Point.X + direction.Y, pointList[i].Point.Y - direction.X);
+
+ if (i == 0)
+ {
+ var direction0 = -direction;
+ var direction1 = Vector2.Transform(direction0, RotationPiDiv8);
+ var direction2 = Vector2.Transform(direction0, RotationPiDiv4);
+ var direction3 = Vector2.Transform(direction0, Rotation3PiDiv8);
+ var directionN1 = new Vector2(direction3.Y, -direction3.X);
+ var directionN2 = new Vector2(direction2.Y, -direction2.X);
+ var directionN3 = new Vector2(direction1.Y, -direction1.X);
+
+ outlinePointList1.Add(new Point(pointList[i].Point.X + direction0.X, pointList[i].Point.Y + direction0.Y));
+ outlinePointList1.Add(new Point(pointList[i].Point.X + directionN1.X, pointList[i].Point.Y + directionN1.Y));
+ outlinePointList1.Add(new Point(pointList[i].Point.X + directionN2.X, pointList[i].Point.Y + directionN2.Y));
+ outlinePointList1.Add(new Point(pointList[i].Point.X + directionN3.X, pointList[i].Point.Y + directionN3.Y));
+
+ outlinePointList2.Add(new Point(pointList[i].Point.X + direction0.X, pointList[i].Point.Y + direction0.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + direction1.X, pointList[i].Point.Y + direction1.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + direction2.X, pointList[i].Point.Y + direction2.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + direction3.X, pointList[i].Point.Y + direction3.Y));
+ }
+
+ outlinePointList1.Add(point1);
+ outlinePointList2.Add(point2);
+ }
+ else if (i == pointList.Count - 1 || pointList[i].Point == pointList[i + 1].Point)
+ {
+ var direction = Vector2.Multiply(halfThickness, Vector2.Normalize(new Vector2((float) pointList[i].Point.X - (float) pointList[i - 1].Point.X, (float) pointList[i].Point.Y - (float) pointList[i - 1].Point.Y)));
+
+ var point1 = new Point(pointList[i].Point.X - direction.Y, pointList[i].Point.Y + direction.X);
+ var point2 = new Point(pointList[i].Point.X + direction.Y, pointList[i].Point.Y - direction.X);
+
+ outlinePointList1.Add(point1);
+ outlinePointList2.Add(point2);
+
+ if (i == pointList.Count - 1)
+ {
+ var rotationPiDiv8 = Matrix3x2.CreateRotation(MathF.PI / 8);
+ var rotationPiDiv4 = Matrix3x2.CreateRotation(MathF.PI / 4);
+ var rotation3PiDiv8 = Matrix3x2.CreateRotation(3 * MathF.PI / 8);
+
+ var direction0 = direction;
+ var direction1 = Vector2.Transform(direction0, rotationPiDiv8);
+ var direction2 = Vector2.Transform(direction0, rotationPiDiv4);
+ var direction3 = Vector2.Transform(direction0, rotation3PiDiv8);
+ var directionN1 = new Vector2(direction3.Y, -direction3.X);
+ var directionN2 = new Vector2(direction2.Y, -direction2.X);
+ var directionN3 = new Vector2(direction1.Y, -direction1.X);
+
+ outlinePointList1.Add(new Point(pointList[i].Point.X + direction3.X, pointList[i].Point.Y + direction3.Y));
+ outlinePointList1.Add(new Point(pointList[i].Point.X + direction2.X, pointList[i].Point.Y + direction2.Y));
+ outlinePointList1.Add(new Point(pointList[i].Point.X + direction1.X, pointList[i].Point.Y + direction1.Y));
+ outlinePointList1.Add(new Point(pointList[i].Point.X + direction0.X, pointList[i].Point.Y + direction0.Y));
+
+ outlinePointList2.Add(new Point(pointList[i].Point.X + directionN3.X, pointList[i].Point.Y + directionN3.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + directionN2.X, pointList[i].Point.Y + directionN2.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + directionN1.X, pointList[i].Point.Y + directionN1.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + direction0.X, pointList[i].Point.Y + direction0.Y));
+ }
+ }
+ else
+ {
+ var direction1 = Vector2.Multiply(halfThickness, Vector2.Normalize(new Vector2((float) pointList[i].Point.X - (float) pointList[i - 1].Point.X, (float) pointList[i].Point.Y - (float) pointList[i - 1].Point.Y)));
+ var direction2 = Vector2.Multiply(halfThickness, Vector2.Normalize(new Vector2((float) pointList[i + 1].Point.X - (float) pointList[i].Point.X, (float) pointList[i + 1].Point.Y - (float) pointList[i].Point.Y)));
+
+ var vector11 = new Vector2(-direction1.Y, direction1.X);
+ var vector12 = new Vector2(direction1.Y, -direction1.X);
+ var vector21 = new Vector2(-direction2.Y, direction2.X);
+ var vector22 = new Vector2(direction2.Y, -direction2.X);
+
+ switch (-direction1.X * direction2.Y + direction1.Y * direction2.X)
+ {
+ case < 0:
+ {
+ var vector1 = Vector2.Normalize(vector11 + vector21) * halfThickness;
+ var vector2 = Vector2.Normalize(vector12 + vector22) * halfThickness;
+
+ outlinePointList1.Add(new Point(pointList[i].Point.X + vector1.X, pointList[i].Point.Y + vector1.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + vector12.X, pointList[i].Point.Y + vector12.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + vector2.X, pointList[i].Point.Y + vector2.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + vector22.X, pointList[i].Point.Y + vector22.Y));
+ break;
+ }
+ case > 0:
+ {
+ var vector1 = Vector2.Normalize(vector11 + vector21) * halfThickness;
+ var vector2 = Vector2.Normalize(vector12 + vector22) * halfThickness;
+
+ outlinePointList1.Add(new Point(pointList[i].Point.X + vector11.X, pointList[i].Point.Y + vector11.Y));
+ outlinePointList1.Add(new Point(pointList[i].Point.X + vector1.X, pointList[i].Point.Y + vector1.Y));
+ outlinePointList1.Add(new Point(pointList[i].Point.X + vector21.X, pointList[i].Point.Y + vector21.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + vector2.X, pointList[i].Point.Y + vector2.Y));
+ break;
+ }
+ default:
+ outlinePointList1.Add(new Point(pointList[i].Point.X + vector11.X, pointList[i].Point.Y + vector11.Y));
+ outlinePointList1.Add(new Point(pointList[i].Point.X + vector21.X, pointList[i].Point.Y + vector21.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + vector12.X, pointList[i].Point.Y + vector12.Y));
+ outlinePointList2.Add(new Point(pointList[i].Point.X + vector22.X, pointList[i].Point.Y + vector22.Y));
+ break;
+ }
+ }
+ }
+
+ var outlinePoints = new Point[outlinePointList1.Count + outlinePointList2.Count + 1];
+ outlinePointList2.Reverse();
+ outlinePointList1.CopyTo(outlinePoints, 0);
+ outlinePointList2.CopyTo(outlinePoints, outlinePointList1.Count);
+ outlinePoints[^1] = outlinePoints[0];
+ return outlinePoints;
+ }
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/Converter.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/Converter.cs
new file mode 100644
index 0000000..551e458
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/Converter.cs
@@ -0,0 +1,17 @@
+using DotNetCampus.Inking.Primitive;
+using DotNetCampus.Numerics.Geometry;
+using WpfInk.PresentationCore.System.Windows;
+using WpfInk.PresentationCore.System.Windows.Input.Stylus;
+
+namespace WpfInk;
+
+static class Converter
+{
+ public static Point2D ToPoint(this Point point) => new Point2D(point.X, point.Y);
+ public static Size2D ToSize(this Size size) => new Size2D(size.Width, size.Height);
+
+ public static StylusPoint ToStylusPoint(this InkStylusPoint stylusPoint)
+ {
+ return new StylusPoint(stylusPoint.X, stylusPoint.Y, stylusPoint.Pressure);
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/IInternalStreamGeometryContext.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/IInternalStreamGeometryContext.cs
new file mode 100644
index 0000000..8734fc8
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/IInternalStreamGeometryContext.cs
@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using WpfInk.PresentationCore.System.Windows;
+
+namespace WpfInk;
+
+internal interface IInternalStreamGeometryContext
+{
+ void BeginFigure(Point startPoint, bool isFilled, bool isClosed);
+ void PolyBezierTo(IList points, bool isStroked, bool isSmoothJoin);
+ void PolyLineTo(IList points, bool isStroked, bool isSmoothJoin);
+ void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, bool sweepDirection, bool isStroked, bool isSmoothJoin);
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/IStreamGeometryContext.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/IStreamGeometryContext.cs
new file mode 100644
index 0000000..ae9fa0b
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/IStreamGeometryContext.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using DotNetCampus.Numerics.Geometry;
+
+namespace WpfInk;
+
+public interface IStreamGeometryContext
+{
+ void BeginFigure(Point2D startPoint, bool isFilled, bool isClosed);
+ void PolyBezierTo(IList points, bool isStroked, bool isSmoothJoin);
+ void PolyLineTo(IList points, bool isStroked, bool isSmoothJoin);
+ void ArcTo(Point2D point, Size2D size, double rotationAngle, bool isLargeArc, bool sweepDirection, bool isStroked, bool isSmoothJoin);
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/InkStrokeRenderer.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/InkStrokeRenderer.cs
new file mode 100644
index 0000000..861455c
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/InkStrokeRenderer.cs
@@ -0,0 +1,30 @@
+using DotNetCampus.Inking.Primitive;
+using MS.Internal.Ink;
+using WpfInk.PresentationCore.System.Windows.Ink;
+using WpfInk.PresentationCore.System.Windows.Input.Stylus;
+
+namespace WpfInk;
+
+public static class InkStrokeRenderer
+{
+ public static void Render(IStreamGeometryContext streamGeometryContext, in StrokeRendererInfo info)
+ {
+ var drawingAttributes = new DrawingAttributes()
+ {
+ Width = info.Width,
+ Height = info.Height,
+ };
+
+ var stylusPointCollection = new StylusPointCollection(info.StylusPointCollection.Count);
+
+ foreach (InkStylusPoint inkStylusPoint2D in info.StylusPointCollection)
+ {
+ stylusPointCollection.Add(inkStylusPoint2D.ToStylusPoint());
+ }
+
+ var stroke = new Stroke(stylusPointCollection, drawingAttributes);
+ StrokeNodeIterator strokeNodeIterator = StrokeNodeIterator.GetIterator(stroke, drawingAttributes);
+ var internalStreamGeometryContext = new InternalStreamGeometryContext(streamGeometryContext);
+ StrokeRenderer.CalcGeometryAndBounds(strokeNodeIterator, drawingAttributes, calculateBounds: false, internalStreamGeometryContext, out _);
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/InternalStreamGeometryContext.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/InternalStreamGeometryContext.cs
new file mode 100644
index 0000000..390051a
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/InternalStreamGeometryContext.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Linq;
+using DotNetCampus.Numerics.Geometry;
+using WpfInk.PresentationCore.System.Windows;
+
+namespace WpfInk;
+
+internal class InternalStreamGeometryContext : IInternalStreamGeometryContext
+{
+ public InternalStreamGeometryContext(IStreamGeometryContext context)
+ {
+ _context = context;
+ }
+
+ private readonly IStreamGeometryContext _context;
+ private readonly List _cacheList = new List();
+
+ public void BeginFigure(Point startPoint, bool isFilled, bool isClosed)
+ {
+ _context.BeginFigure(startPoint.ToPoint(), isFilled, isClosed);
+ }
+
+ public void PolyBezierTo(IList points, bool isStroked, bool isSmoothJoin)
+ {
+ _cacheList.Clear();
+ _cacheList.AddRange(points.Select(t => t.ToPoint()));
+ _context.PolyBezierTo(_cacheList, isStroked, isSmoothJoin);
+ }
+
+ public void PolyLineTo(IList points, bool isStroked, bool isSmoothJoin)
+ {
+ _cacheList.Clear();
+ _cacheList.AddRange(points.Select(t => t.ToPoint()));
+ _context.PolyLineTo(_cacheList, isStroked, isSmoothJoin);
+ }
+
+ public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, bool sweepDirection, bool isStroked,
+ bool isSmoothJoin)
+ {
+ _context.ArcTo(point.ToPoint(), size.ToSize(), rotationAngle, isLargeArc, sweepDirection, isStroked, isSmoothJoin);
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/StrokeRendererInfo.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/StrokeRendererInfo.cs
new file mode 100644
index 0000000..9bd2998
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/API/StrokeRendererInfo.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using DotNetCampus.Inking.Primitive;
+
+namespace WpfInk;
+
+public readonly record struct StrokeRendererInfo
+{
+ public required IReadOnlyList StylusPointCollection { get; init; }
+
+ public required double Width { get; init; }
+ public required double Height { get; init; }
+
+ public bool FitToCurve { get; init; }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/Bezier.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/Bezier.cs
new file mode 100644
index 0000000..b735a30
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/Bezier.cs
@@ -0,0 +1,587 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Diagnostics;
+using System.Windows;
+using System.Windows.Input;
+using System.Collections.Generic;
+
+using MS.Internal.Ink.InkSerializedFormat;
+using WpfInk.PresentationCore.System.Windows;
+using WpfInk.PresentationCore.System.Windows.Input.Stylus;
+
+namespace MS.Internal.Ink
+{
+ ///
+ /// Bezier curve generation class
+ ///
+ internal class Bezier
+ {
+ ///
+ /// Default constructor
+ ///
+ public Bezier() { }
+
+ ///
+ /// Construct bezier control points from points
+ ///
+ /// Original StylusPointCollection
+ /// Fitting error
+ /// Whether the algorithm succeeded
+ internal bool ConstructBezierState(StylusPointCollection stylusPoints, double fitError)
+ {
+ // If the point count is zero, the curve cannot be constructed
+ if ((null == stylusPoints) || (stylusPoints.Count == 0))
+ return false;
+
+ // Compile list of distinct points and their nodes
+ CuspData dat = new CuspData();
+ dat.Analyze(stylusPoints,
+ fitError /*typically zero*/);
+
+ return ConstructFromData(dat, fitError);
+ }
+
+ ///
+ /// Flatten bezier with a given resolution
+ ///
+ /// tolerance
+ internal List Flatten(double tolerance)
+ {
+ List points = new List();
+
+ // First point
+ Vector vector = GetBezierPoint(0);
+ points.Add(new Point(vector.X, vector.Y));
+
+ int last = this.BezierPointCount - 4;
+
+ if (0 <= last)
+ {
+ // Tolerance needs to be non-zero positive
+ if (tolerance < DoubleUtil.DBL_EPSILON)
+ tolerance = DoubleUtil.DBL_EPSILON;
+
+ // Flatten individual segments
+ for (int i = 0; i <= last; i += 3)
+ FlattenSegment(i, tolerance, points);
+ }
+
+ //convert from himetric to Avalon
+ for (int x = 0; x < points.Count; x++)
+ {
+ Point p = points[x];
+ p.X *= StrokeCollectionSerializer.HimetricToAvalonMultiplier;
+ p.Y *= StrokeCollectionSerializer.HimetricToAvalonMultiplier;
+ points[x] = p;
+ }
+
+ return points;
+ }
+
+
+ ///
+ /// Extend the current bezier segment if possible
+ ///
+ /// Fitting error sqaure
+ /// Data points
+ /// Starting index
+ /// NExt cusp index
+ /// Index of the last index, updated here
+ /// Whether there is a cusp at the end
+ /// Whether end of the stroke is reached
+ /// Whether the the segment was extended
+ private bool ExtendingRange(double error, CuspData data, int from, int next_cusp, ref int to, ref bool cusp, ref bool done)
+ {
+ to++;
+ cusp = true; // Presumed guilty
+ done = to >= data.Count - 1;
+ if (done)
+ {
+ to = data.Count - 1;
+ cusp = true;
+ return false;
+ }
+
+ cusp = to >= next_cusp;
+ if (cusp)
+ {
+ to = next_cusp;
+ return false;
+ }
+
+ Debug.Assert(to - from >= 4);
+ int d = (to - from) / 4;
+ int[] i = { from, from + d, (to + from) / 2, to - d, to };
+
+ // Test for "cubicness"
+ return CoCubic(data, i, error);
+ }
+
+
+ ///
+ /// Add a bezier segment to the bezier buffer
+ ///
+ /// In: Data points
+ /// In: Index of the first point
+ /// In: Unit tangent vector at the start
+ /// In: Index of the last point, updated here
+ /// In: Unit tangent vector at the end
+ /// True if the segment was added
+ private bool AddBezierSegment(CuspData data, int from, ref Vector tanStart, int to, ref Vector tanEnd)
+ {
+ switch (to - from)
+ {
+ case 1:
+ AddLine(data, from, to);
+ return true;
+
+ case 2:
+ AddParabola(data, from);
+ return true;
+ }
+
+ // We have at least 4 points, compute a least squares cubic
+ return AddLeastSquares(data, from, ref tanStart, to, ref tanEnd);
+ }
+
+
+ ///
+ /// Construct bezier curve from data points
+ ///
+ /// In: Data points
+ /// In: tolerated error
+ /// Whether bezier construction is possible
+ private bool ConstructFromData(CuspData data, double fitError)
+ {
+ // Check for empty stroke
+ if (data.Count < 2)
+ {
+ return false;
+ }
+
+ // Add the first point
+ AddBezierPoint(data.XY(0));
+
+ // Special cases - 2 or 3 points
+ if (data.Count == 3)
+ {
+ AddParabola(data, 0);
+ return true;
+ }
+ else if (data.Count == 2)
+ {
+ AddLine(data, 0, 1);
+ return true;
+ }
+
+ // For default case error passed in will be 0.
+ // 3% is the default value
+ if (DoubleUtil.DBL_EPSILON > fitError)
+ fitError = 0.03f * (data.Distance() * StrokeCollectionSerializer.HimetricToAvalonMultiplier);
+
+ data.SetTanLinks(0.5f * fitError);
+
+ // otherwise use the value specified in the drawing attribute
+ // get (error)^2
+ fitError *= (fitError);
+
+ bool done = false;
+ int to = 0;
+ int next_cusp = 0;
+ int prev_cusp = 0;
+ bool is_a_cusp = true;
+ Vector tanEnd = new Vector(0, 0);
+ Vector tanStart = new Vector(0, 0);
+
+ for (int from = 0; !done; from = to)
+ {
+ if (is_a_cusp)
+ {
+ prev_cusp = next_cusp;
+ next_cusp = data.GetNextCusp(from);
+ if (!data.Tangent(ref tanStart, from, prev_cusp, next_cusp, false, true))
+ {
+ return false;
+ }
+ }
+ else
+ {
+ tanStart.X = -tanEnd.X;
+ tanStart.Y = -tanEnd.Y;
+ }
+
+ to = from + 3;
+
+ // No meat in this loop, just extending the index range
+ while (ExtendingRange(fitError, data, from, next_cusp, ref to, ref is_a_cusp, ref done)) ;
+
+ // Find the tangent
+ if (!data.Tangent(ref tanEnd, to, prev_cusp, next_cusp, true, is_a_cusp))
+ {
+ return false;
+ }
+
+ // Add bezier segment
+ if (!AddBezierSegment(data, from, ref tanStart, to, ref tanEnd))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+
+ ///
+ /// Add parabola to the bezier
+ ///
+ /// In: Data points
+ /// In: The index of the parabola's first point
+ private void AddParabola(CuspData data, int from)
+ {
+ /* Denote s = 1-t. We construct the parabola with Bezier points A,B,C that
+ goes thru the point P at parameter value t, that is
+ P = s^2A + 2stB + t^2C
+
+ We know A and C, and we solve for B:
+ B = (P - s^2A - t^2C) / 2st.
+
+ Elevating the degree to cubic replaces B with 2 points, the first at
+ 2B/3 + A/3, and the second at 2B/3 + C/3.
+
+ That is, one point at
+ (P/(st) - Ct/s + A(-s/t + 1)) / 3
+ and the other point at
+ (P/(st) + C(-t/s + 1) - As/t) / 3
+ */
+ // By the way the nodes were constructed:
+ //ASSERT(data.Node(from+2) - data.Node(from) >
+ // data.Node(from+1) - data.Node(from));
+ double t = (data.Node(from + 1) - data.Node(from)) / (data.Node(from + 2) - data.Node(from));
+ double s = 1 - t;
+
+ if (t < .001 || s < .001)
+ {
+ // A straight line will be a better approximation
+ AddLine(data, from, from + 2);
+ return;
+ }
+
+ double tt = 1 / t;
+ double ss = 1 / s;
+ const double third = 1.0d / 3.0d;
+ Vector P = (tt * ss) * data.XY(from + 1);
+ Vector B = third * (P + (1 - s * tt) * data.XY(from) - (t * ss) * data.XY(from + 1));
+
+ AddBezierPoint(B);
+ B = third * (P - (s * tt) * data.XY(from) + (1 - t * ss) * data.XY(from + 2));
+ AddBezierPoint(B);
+ AddSegmentPoint(data, from + 2);
+ }
+
+
+ ///
+ /// Add Line to the bezier
+ ///
+ /// In: Data points
+ /// In: The index of the line's first point
+ /// In: The index of the line's last point
+ private void AddLine(CuspData data, int from, int to)
+ {
+ const double third = 1.0d / 3.0d;
+
+ AddBezierPoint((2 * data.XY(from) + data.XY(to)) * third);
+ AddBezierPoint((data.XY(from) + 2 * data.XY(to)) * third);
+ AddSegmentPoint(data, to);
+ }
+
+
+ ///
+ /// Add least square fit curve to the bezier
+ ///
+ /// In: Data points
+ /// In: Index of the first point
+ /// In: Unit tangent vector at the start
+ /// In: Index of the last point, updated here
+ /// In: Unit tangent vector at the end
+ /// Return true segment added
+ private bool AddLeastSquares(CuspData data, int from, ref Vector V, int to, ref Vector W)
+ {
+ /* To do: When there is a cusp at either one of the ends, we'll get a
+ better approximation if we use a construction without a prescribed
+ tangent there */
+ /*
+ The Bezier points of this segment are A, A+sV, B+uW, and B, where A,B are the
+ endpoints, and V,W are the end tangents. For the node tj, denote f0j=(1-tj)^3,
+ f1j=3(1-tj)^2tj, f2j=3(1-tj)tj^2, f3j=tj^3. Let Pj be the jth point.
+ We are lookig for s,u that minimize
+ Sum(A*f0j + (A+sV)*f1j + (B+uW)*f2j + B*f3j - Pj)^2.
+
+ Equate the partial derivatives of this w.r.t. s and u to 0:
+ Sum(A*f0j + (A+sV)*f1j + (B+uW)*f2j + B*f3j - Pj)*(V*f1j)=0
+ Sum(A*f0j + (A+sV)*f1j + (B+uW)*f2j + B*f3j - Pj)*(W*f2j)=0
+ hence
+
+ s*Sum(V*V*f1j*f1j) + u*Sum(W*V*f1j*f2j)= -Sum(A*(f0j+f1j) + B*(f2j+f3j) - Pj)*V*f1j
+ s*Sum(V*W*f1j*f2j) + u*Sum(W*W*f2j*f2j)= -Sum(A*(f0j+f1j) + B*(f2j+f3j) - Pj)*W*f2j
+
+ so the equations are
+ s*a11 + u*a12 = b1
+ s*a12 * u*a22 = b2
+
+ with
+ a11 = W*W*Sum(f1j^2), a22 = V*V*Sum(f2j^2), a12 = W*V*Sum(f1j*f2j)
+ b1 = -V*A*Sum(f0j + f1j)*f1j - V*B*Sum(f2j + f3j)*f1j + Sum(f1j*Pj*V)
+ b2 = -W*A*Sum(f0j + f1j)*f2j - W*B*Sum(f2j + f3j)*f2j + Sum(f2j*Pj*W)
+
+ V and W ae unit vectors, so V*V = W*W = 1.
+ For computational efficiency, we will break b1 and b2 into 3 sums each, and add
+ them up at the end
+
+ The solution is
+ s = (b1*a22 - b2*a12) / det
+ u = (b2*a11 - b1*a12) / det
+ where det = a11*a22 - a22^2
+ */
+ // Compute the coefficients
+ double a11 = 0, a12 = 0, a22 = 0, b1 = 0, b2 = 0;
+ double b11 = 0, b12 = 0, b21 = 0, b22 = 0;
+
+ for (int j = checked(from + 1); j < to; j++)
+ {
+ // By the way the nodes were constructed -
+ Debug.Assert(data.Node(to) - data.Node(from) > data.Node(j) - data.Node(from));
+ double tj = (data.Node(j) - data.Node(from)) / (data.Node(to) - data.Node(from));
+ double tj2 = tj * tj;
+ double rj = 1 - tj;
+ double rj2 = rj * rj;
+
+ double f0j = rj2 * rj;
+ double f1j = 3 * rj2 * tj;
+ double f2j = 3 * rj * tj2;
+ double f3j = tj2 * tj;
+
+ a11 += f1j * f1j;
+ a22 += f2j * f2j;
+ a12 += f1j * f2j;
+
+ b11 -= (f0j + f1j) * f1j;
+ b12 -= (f2j + f3j) * f1j;
+ b1 += f1j * (data.XY(j) * V);
+
+ b21 -= (f0j + f1j) * f2j;
+ b22 -= (f2j + f3j) * f2j;
+ b2 += f2j * (data.XY(j) * W);
+ }
+
+ a12 *= (V * W);
+ b1 += ((V * data.XY(from)) * b11 + (V * data.XY(to)) * b12);
+ b2 += ((W * data.XY(from)) * b21 + (W * data.XY(to)) * b22);
+
+ // Solve the equations
+ double s = b1 * a22 - b2 * a12;
+ double u = b2 * a11 - b1 * a12;
+ double det = a11 * a22 - a12 * a12;
+ bool accept = (Math.Abs(det) > Math.Abs(s) * DoubleUtil.DBL_EPSILON &&
+ Math.Abs(det) > Math.Abs(u) * DoubleUtil.DBL_EPSILON);
+
+ if (accept)
+ {
+ s /= det;
+ u /= det;
+
+ // We'll only accept large enough positive solutions
+ accept = s > 1.0e-6 && u > 1.0e-6;
+ }
+
+ if (!accept)
+ s = u = (data.Node(to) - data.Node(from)) / 3;
+
+ AddBezierPoint(data.XY(from) + s * V);
+ AddBezierPoint(data.XY(to) + u * W);
+ AddSegmentPoint(data, to);
+ return true;
+ }
+
+
+ ///
+ /// Checks whether five points are co-cubic within tolerance
+ ///
+ /// In: Data points
+ /// In: Array of 5 indices
+ /// In: tolerated error - squared
+ /// Return true if extended
+ private static bool CoCubic(CuspData data, int[] i, double fitError)
+ {
+ /* Our error estimate is (t[4]-t[0])^4 times the 4th divided difference
+ * of the points with resect to the nodes. The divided difference is
+ * equal to Sum(c(i)*p[i]), where c(i)=Product(t[i]-t[j]: j != i)
+ * (See Conte & deBoor's Elementary Numerical Analysis, Excercise 2.2-1).
+ * We multiply each factor in the product by t[4]-t[0].
+ */
+ double d04 = data.Node(i[4]) - data.Node(i[0]);
+ double d01 = d04 / (data.Node(i[1]) - data.Node(i[0]));
+ double d02 = d04 / (data.Node(i[2]) - data.Node(i[0]));
+ double d03 = d04 / (data.Node(i[3]) - data.Node(i[0]));
+ double d12 = d04 / (data.Node(i[2]) - data.Node(i[1]));
+ double d13 = d04 / (data.Node(i[3]) - data.Node(i[1]));
+ double d14 = d04 / (data.Node(i[4]) - data.Node(i[1]));
+ double d23 = d04 / (data.Node(i[3]) - data.Node(i[2]));
+ double d24 = d04 / (data.Node(i[4]) - data.Node(i[2]));
+ double d34 = d04 / (data.Node(i[4]) - data.Node(i[3]));
+ Vector P = d01 * d02 * d03 * data.XY(i[0]) -
+ d01 * d12 * d13 * d14 * data.XY(i[1]) +
+ d02 * d12 * d23 * d24 * data.XY(i[2]) -
+ d03 * d13 * d23 * d34 * data.XY(i[3]) +
+ d14 * d24 * d34 * data.XY(i[4]);
+
+ return ((P * P) < fitError);
+ }
+
+
+ ///
+ /// Add Bezier point to the output buffer
+ ///
+ /// In: The point to add
+ private void AddBezierPoint(Vector point)
+ {
+ _bezierControlPoints.Add((Point) point);
+ }
+
+
+ ///
+ /// Add segment point
+ ///
+ /// In: Interpolation data
+ /// In: The index of the point to add
+ private void AddSegmentPoint(CuspData data, int index)
+ {
+ _bezierControlPoints.Add((Point) data.XY(index));
+ }
+
+
+ ///
+ /// Evaluate on a Bezier segment a point at a given parameter
+ ///
+ /// Index of Bezier segment's first point
+ /// Parameter value t
+ /// Return the point at parameter t on the curve
+ private Vector DeCasteljau(int iFirst, double t)
+ {
+ // Using the de Casteljau algorithm. See "Curves & Surfaces for Computer
+ // Aided Design" for the theory
+ double s = 1.0f - t;
+
+ // Level 1
+ Vector Q0 = s * GetBezierPoint(iFirst) + t * GetBezierPoint(iFirst + 1);
+ Vector Q1 = s * GetBezierPoint(iFirst + 1) + t * GetBezierPoint(iFirst + 2);
+ Vector Q2 = s * GetBezierPoint(iFirst + 2) + t * GetBezierPoint(iFirst + 3);
+
+ // Level 2
+ Q0 = s * Q0 + t * Q1;
+ Q1 = s * Q1 + t * Q2;
+
+ // Level 3
+ return s * Q0 + t * Q1;
+ }
+
+ ///
+ /// Flatten a Bezier segment within given resolution
+ ///
+ /// Index of Bezier segment's first point
+ /// tolerance
+ ///
+ ///
+ private void FlattenSegment(int iFirst, double tolerance, List points)
+ {
+ // We use forward differencing. It is much faster than subdivision
+ int i, k;
+ int nPoints = 1;
+ Vector[] Q = new Vector[4];
+
+ // The number of points is determined by the "curvedness" of this segment,
+ // which is a heuristic: it's the maximum of the 2 medians of the triangles
+ // formed by consecutive Bezier points. Why median? because it is cheaper
+ // to compute than height.
+ double rCurv = 0;
+
+ for (i = checked(iFirst + 1); i <= checked(iFirst + 2); i++)
+ {
+ // Get the longer median
+ Q[0] = (GetBezierPoint(i - 1) + GetBezierPoint(i + 1)) * 0.5f - GetBezierPoint(i);
+
+ double r = Q[0].Length;
+
+ if (r > rCurv)
+ rCurv = r;
+ }
+
+ // Now we look at the ratio between the medain and the error tolerance.
+ // the points are collinear then one point - the endpoint - will do.
+ // Otherwise, since curvature is roughly inverse proportional
+ // to the square of nPoints, we set nPoints to be the square root of this
+ // ratio, but not less than 3.
+ if (rCurv <= 0.5 * tolerance) // Flat segment
+ {
+ Vector vector = GetBezierPoint(iFirst + 3);
+ points.Add(new Point(vector.X, vector.Y));
+ return;
+ }
+
+ // Otherwise we'll have at least 3 points
+ // Tolerance is assumed to be positive
+ nPoints = (int) (Math.Sqrt(rCurv / tolerance)) + 3;
+ if (nPoints > 1000)
+ nPoints = 1000; // Arbitrary limitation, but...
+
+ // Get the first 4 points on the segment in the buffer
+ double d = 1.0f / (double) nPoints;
+
+ Q[0] = GetBezierPoint(iFirst);
+ for (i = 1; i <= 3; i++)
+ {
+ Q[i] = DeCasteljau(iFirst, i * d);
+ points.Add(new Point(Q[i].X, Q[i].Y));
+ }
+
+ // Replace points in the buffer with differences of various levels
+ for (i = 1; i <= 3; i++)
+ for (k = 0; k <= (3 - i); k++)
+ Q[k] = Q[k + 1] - Q[k];
+
+ // Now generate the rest of the points by forward differencing
+ for (i = 4; i <= nPoints; i++)
+ {
+ for (k = 1; k <= 3; k++)
+ Q[k] += Q[k - 1];
+
+ points.Add(new Point(Q[3].X, Q[3].Y));
+ }
+ }
+ ///
+ /// Returns a single bezier control point at index
+ ///
+ /// Index
+ ///
+ private Vector GetBezierPoint(int index)
+ {
+ return (Vector) _bezierControlPoints[index];
+ }
+
+
+ ///
+ /// Count of bezier control points
+ ///
+ private int BezierPointCount
+ {
+ get { return _bezierControlPoints.Count; }
+ }
+
+ // Bezier points
+ private List _bezierControlPoints = new List();
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ContourSegment.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ContourSegment.cs
new file mode 100644
index 0000000..d50df24
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ContourSegment.cs
@@ -0,0 +1,70 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Windows;
+using WpfInk.PresentationCore.System.Windows;
+
+namespace MS.Internal.Ink
+{
+ ///
+ /// A helper structure representing an edge of a contour, where
+ /// the edge is either a straight segment or an arc of a circle.
+ /// ContourSegment are alwais directed clockwise (i.e with the contour
+ /// inner area being on the right side.
+ /// Used in hit-testing a contour vs another contour.
+ ///
+ internal readonly struct ContourSegment
+ {
+ ///
+ /// Constructor for linear segments
+ ///
+ /// segment's begin point
+ /// segment's end point
+ internal ContourSegment(Point begin, Point end)
+ {
+ _begin = begin;
+ _vector = DoubleUtil.AreClose(begin, end) ? new Vector(0, 0) : (end - begin);
+ _radius = new Vector(0, 0);
+ }
+
+ ///
+ /// Constructor for arcs
+ ///
+ /// arc's begin point
+ /// arc's end point
+ /// arc's center
+ internal ContourSegment(Point begin, Point end, Point center)
+ {
+ _begin = begin;
+ _vector = end - begin;
+ _radius = center - begin;
+ }
+
+ /// Tells whether the segment is arc or straight
+ internal bool IsArc { get { return (_radius.X != 0) || (_radius.Y != 0); } }
+
+ /// Returns the begin point of the segment
+ internal Point Begin { get { return _begin; } }
+
+ /// Returns the end point of the segment
+ internal Point End { get { return _begin + _vector; } }
+
+ /// Returns the vector from Begin to End
+ internal Vector Vector { get { return _vector; } }
+
+ /// Returns the vector from Begin to the center of the circle
+ /// (zero vector for linear segments
+ internal Vector Radius { get { return _radius; } }
+
+ #region Fields
+
+ private readonly Point _begin;
+ private readonly Vector _vector;
+ private readonly Vector _radius;
+
+ #endregion
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/CuspData.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/CuspData.cs
new file mode 100644
index 0000000..2235d51
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/CuspData.cs
@@ -0,0 +1,548 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Diagnostics;
+using System.Windows;
+using System.Windows.Input;
+using System.Collections.Generic;
+using MS.Internal.Ink.InkSerializedFormat;
+using WpfInk.PresentationCore.System.Windows;
+using WpfInk.PresentationCore.System.Windows.Input.Stylus;
+
+namespace MS.Internal.Ink
+{
+ internal class CuspData
+ {
+ ///
+ /// Default constructor
+ ///
+ internal CuspData()
+ {
+ }
+
+ ///
+ /// Constructs internal data structure from points for doing operations like
+ /// cusp detection or tangent computation
+ ///
+ /// Points to be analyzed
+ /// Distance between two consecutive distinct points
+ internal void Analyze(StylusPointCollection stylusPoints, double rSpan)
+ {
+ // If the count is less than 1, return
+ if ((null == stylusPoints) || (stylusPoints.Count == 0))
+ return;
+
+ _points = new List(stylusPoints.Count);
+ _nodes = new List(stylusPoints.Count);
+
+ // Construct the lists of data points and nodes
+ _nodes.Add(0);
+ CDataPoint cdp0 = new CDataPoint();
+ cdp0.Index = 0;
+ //convert from Avalon to Himetric
+ Point point = (Point) stylusPoints[0];
+ point.X *= StrokeCollectionSerializer.AvalonToHimetricMultiplier;
+ point.Y *= StrokeCollectionSerializer.AvalonToHimetricMultiplier;
+ cdp0.Point = point;
+ _points.Add(cdp0);
+
+ //drop duplicates
+ int index = 0;
+ for (int i = 1; i < stylusPoints.Count; i++)
+ {
+ if (!DoubleUtil.AreClose(stylusPoints[i].X, stylusPoints[i - 1].X) ||
+ !DoubleUtil.AreClose(stylusPoints[i].Y, stylusPoints[i - 1].Y))
+ {
+ //this is a unique point, add it
+ index++;
+
+ CDataPoint cdp = new CDataPoint();
+ cdp.Index = index;
+
+ //convert from Avalon to Himetric
+ Point point2 = (Point) stylusPoints[i];
+ point2.X *= StrokeCollectionSerializer.AvalonToHimetricMultiplier;
+ point2.Y *= StrokeCollectionSerializer.AvalonToHimetricMultiplier;
+ cdp.Point = point2;
+
+ _points.Insert(index, cdp);
+ _nodes.Insert(index, _nodes[index - 1] + (XY(index) - XY(index - 1)).Length);
+ }
+ }
+
+ SetLinks(rSpan);
+ }
+
+ ///
+ /// Set links amongst the points for tangent computation
+ ///
+ /// Shortest distance between two distinct points
+ internal void SetTanLinks(double rError)
+ {
+ int count = Count;
+
+ if (rError < 1.0)
+ rError = 1.0f;
+
+ for (int i = 0; i < count; ++i)
+ {
+ // Find a StylusPoint at distance-_span forward
+ for (int j = i + 1; j < count; j++)
+ {
+ if (_nodes[j] - _nodes[i] >= rError)
+ {
+ CDataPoint cdp = _points[i];
+ cdp.TanNext = j;
+ _points[i] = cdp;
+
+ CDataPoint cdp2 = _points[j];
+ cdp2.TanPrev = i;
+ _points[j] = cdp2;
+ break;
+ }
+ }
+
+ if (0 > _points[i].TanPrev)
+ {
+ for (int j = i - 1; 0 <= j; --j)
+ {
+ if (_nodes[i] - _nodes[j] >= rError)
+ {
+ CDataPoint cdp = _points[i];
+ cdp.TanPrev = j;
+ _points[i] = cdp;
+ break;
+ }
+ }
+ }
+
+ if (0 > _points[i].TanNext)
+ {
+ CDataPoint cdp = _points[i];
+ cdp.TanNext = count - 1;
+ _points[i] = cdp;
+ }
+
+ if (0 > _points[i].TanPrev)
+ {
+ CDataPoint cdp = _points[i];
+ cdp.TanPrev = 0;
+ _points[i] = cdp;
+ }
+ }
+ }
+
+
+
+
+ ///
+ /// Return the Index of the next cusp or the
+ /// Index of the last StylusPoint if no cusp was found
+ ///
+ /// Current StylusPoint Index
+ /// Index into CuspData object for the next cusp
+ internal int GetNextCusp(int iCurrent)
+ {
+ int last = Count - 1;
+
+ if (iCurrent < 0)
+ return 0;
+
+ if (iCurrent >= last)
+ return last;
+
+ // Perform a binary search
+ int s = 0, e = _cusps.Count;
+ int m = (s + e) / 2;
+
+ while (s < m)
+ {
+ if (_cusps[m] <= iCurrent)
+ s = m;
+ else
+ e = m;
+
+ m = (s + e) / 2;
+ }
+
+ return _cusps[m + 1];
+ }
+
+ ///
+ /// Point at Index i into the cusp data structure
+ ///
+ /// Index
+ /// StylusPoint
+ /// The Index is within the bounds
+ internal Vector XY(int i)
+ {
+ return new Vector(_points[i].Point.X, _points[i].Point.Y);
+ }
+
+
+ ///
+ /// Number of points in the internal data structure
+ ///
+ internal int Count
+ {
+ get { return _points.Count; }
+ }
+
+
+ ///
+ /// Returns the chord length of the i-th StylusPoint from start of the stroke
+ ///
+ /// StylusPoint Index
+ /// distance
+ /// The Index is within the bounds
+ internal double Node(int i)
+ {
+ return _nodes[i];
+ }
+
+
+ ///
+ /// Returns the Index into original points given an Index into cusp data
+ ///
+ /// Cusp data Index
+ /// Original StylusPoint Index
+ internal int GetPointIndex(int nodeIndex)
+ {
+ return _points[nodeIndex].Index;
+ }
+
+
+ ///
+ /// Distance
+ ///
+ /// distance
+ internal double Distance()
+ {
+ return _dist;
+ }
+
+
+ ///
+ /// Finds the approximante tangent at a given StylusPoint
+ ///
+ /// Tangent vector
+ /// Index at which the tangent is calculated
+ /// Index of the previous cusp
+ /// Index of the next cusp
+ /// Forward or reverse tangent
+ /// Whether the current idex is a cusp StylusPoint
+ /// Return whether the tangent computation succeeded
+ internal bool Tangent(ref Vector ptT, int nAt, int nPrevCusp, int nNextCusp, bool bReverse, bool bIsCusp)
+ {
+ // Tangent is computed as the unit vector along
+ // PT = (P1 - P0) + (P2 - P0) + (P3 - P0)
+ // => PT = P1 + P2 + P3 - 3 * P0
+ int i_1, i_2, i_3;
+
+ if (bIsCusp)
+ {
+ if (bReverse)
+ {
+ i_1 = _points[nAt].TanPrev;
+ if (i_1 < nPrevCusp || (0 > i_1))
+ {
+ i_2 = nPrevCusp;
+ i_1 = (i_2 + nAt) / 2;
+ }
+ else
+ {
+ i_2 = _points[i_1].TanPrev;
+ if (i_2 < nPrevCusp)
+ i_2 = nPrevCusp;
+ }
+ }
+ else
+ {
+ i_1 = _points[nAt].TanNext;
+ if (i_1 > nNextCusp || (0 > i_1))
+ {
+ i_2 = nNextCusp;
+ i_1 = (i_2 + nAt) / 2;
+ }
+ else
+ {
+ i_2 = _points[i_1].TanNext;
+ if (i_2 > nNextCusp)
+ i_2 = nNextCusp;
+ }
+ }
+ ptT = XY(i_1) + 0.5 * XY(i_2) - 1.5 * XY(nAt);
+ }
+ else
+ {
+ Debug.Assert(bReverse);
+ i_1 = nAt;
+ i_2 = _points[nAt].TanPrev;
+ if (i_2 < nPrevCusp)
+ {
+ i_3 = nPrevCusp;
+ i_2 = (i_3 + i_1) / 2;
+ }
+ else
+ {
+ i_3 = _points[i_2].TanPrev;
+ if (i_3 < nPrevCusp)
+ i_3 = nPrevCusp;
+ }
+
+ nAt = _points[nAt].TanNext;
+ if (nAt > nNextCusp)
+ nAt = nNextCusp;
+
+ ptT = XY(i_1) + XY(i_2) + 0.5 * XY(i_3) - 2.5 * XY(nAt);
+ }
+
+ if (DoubleUtil.IsZero(ptT.LengthSquared))
+ {
+ return false;
+ }
+
+ ptT.Normalize();
+ return true;
+ }
+
+ ///
+ /// This "curvature" is not the theoretical curvature. it is a number between
+ /// 0 and 2 that is defined as 1 - cos(angle between segments) at this StylusPoint.
+ ///
+ /// Previous data StylusPoint Index
+ /// Current data StylusPoint Index
+ /// Next data StylusPoint Index
+ /// "Curvature"
+ private double GetCurvature(int iPrev, int iCurrent, int iNext)
+ {
+ Vector V = XY(iCurrent) - XY(iPrev);
+ Vector W = XY(iNext) - XY(iCurrent);
+ double r = V.Length * W.Length;
+
+ if (DoubleUtil.IsZero(r))
+ return 0;
+
+ return 1 - (V * W) / r;
+ }
+
+
+ ///
+ /// Find all cusps for the stroke
+ ///
+ private void FindAllCusps()
+ {
+ // Clear the existing cusp indices
+ _cusps.Clear();
+
+ // There is nothing to find out from
+ if (1 > this.Count)
+ return;
+
+ // First StylusPoint is always a cusp
+ _cusps.Add(0);
+
+ int iPrev = 0, iNext = 0, iCuspPrev = 0;
+
+ // Find the next StylusPoint for Index 0
+ // The following check will cover coincident points, stroke with
+ // less than 3 points
+ if (!FindNextAndPrev(0, iCuspPrev, ref iPrev, ref iNext))
+ {
+ // Point count is zero, thus, there can't be any cusps
+ if (0 == this.Count)
+ _cusps.Clear();
+ else if (1 < this.Count) // Last StylusPoint is always a cusp
+ _cusps.Add(iNext);
+
+ return;
+ }
+
+ // Start the algorithm with the next StylusPoint
+ int iPoint = iNext;
+ double rCurv = 0;
+
+ // Check all the points on the chord of the stroke
+ while (FindNextAndPrev(iPoint, iCuspPrev, ref iPrev, ref iNext))
+ {
+ // Find the curvature at iPoint
+ rCurv = GetCurvature(iPrev, iPoint, iNext);
+
+ /*
+ We'll look at every StylusPoint where rPrevCurv is a local maximum, and the
+ curvature is more than the noise threashold. If we're near the beginning
+ of the stroke then we'll ignore it and carry on. If we're near the end
+ then we'll skip to the end. Otherwise, we'll flag it as a cusp if it
+ deviates is significantly from the curvature at nearby points, forward
+ and backward
+ */
+ if (0.80 < rCurv)
+ {
+ double rMaxCurv = rCurv;
+ int iMaxCurv = iPoint;
+ int m = 0, k = 0;
+
+ if (!FindNextAndPrev(iNext, iCuspPrev, ref k, ref m))
+ {
+ // End of the stroke has been reached
+ break;
+ }
+
+ for (int i = iPrev + 1; (i <= m) && FindNextAndPrev(i, iCuspPrev, ref iPrev, ref iNext); ++i)
+ {
+ rCurv = GetCurvature(iPrev, i, iNext);
+ if (rCurv > rMaxCurv)
+ {
+ rMaxCurv = rCurv;
+ iMaxCurv = i;
+ }
+ }
+
+ // Save the Index with max curvature
+ _cusps.Add(iMaxCurv);
+
+ // Continue the search with next StylusPoint
+ iPoint = m + 1;
+ iCuspPrev = iMaxCurv;
+ }
+ else if (0.035 > rCurv)
+ {
+ // If the angle is less than 15 degree, skip the segment
+ iPoint = iNext;
+ }
+ else
+ ++iPoint;
+ }
+
+ // If everything went right, add the last StylusPoint to the list of cusps
+ _cusps.Add(this.Count - 1);
+ }
+
+
+ ///
+ /// Finds the next and previous data StylusPoint Index for the given data Index
+ ///
+ /// Index at which the computation is performed
+ /// Previous cusp
+ /// Previous data Index
+ /// Next data Index
+ /// Returns true if the end has NOT been reached.
+ private bool FindNextAndPrev(int iPoint, int iPrevCusp, ref int iPrev, ref int iNext)
+ {
+ bool bHasMore = true;
+
+ if (iPoint >= Count)
+ {
+ bHasMore = false;
+ iPoint = Count - 1;
+ }
+
+ // Find a StylusPoint at distance-_span forward
+ for (iNext = checked(iPoint + 1); iNext < Count; ++iNext)
+ if (_nodes[iNext] - _nodes[iPoint] >= _span)
+ break;
+
+ if (iNext >= Count)
+ {
+ bHasMore = false;
+ iNext = Count - 1;
+ }
+
+ for (iPrev = checked(iPoint - 1); iPrevCusp <= iPrev; --iPrev)
+ if (_nodes[iPoint] - _nodes[iPrev] >= _span)
+ break;
+
+ if (iPrev < 0)
+ iPrev = 0;
+
+ return bHasMore;
+ }
+
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private static void UpdateMinMax(double a, ref double rMin, ref double rMax)
+ {
+ rMin = Math.Min(rMin, a);
+ rMax = Math.Max(a, rMax);
+ }
+ ///
+ /// Sets up the internal data structure to construct chain of points
+ ///
+ /// Shortest distance between two distinct points
+ private void SetLinks(double rSpan)
+ {
+ // NOP, if there is only one StylusPoint
+ int count = Count;
+
+ if (2 > count)
+ return;
+
+ // Set up the links to next and previous probe
+ double rL = XY(0).X;
+ double rT = XY(0).Y;
+ double rR = rL;
+ double rB = rT;
+
+ for (int i = 0; i < count; ++i)
+ {
+ UpdateMinMax(XY(i).X, ref rL, ref rR);
+ UpdateMinMax(XY(i).Y, ref rT, ref rB);
+ }
+
+ rR -= rL;
+ rB -= rT;
+ _dist = Math.Abs(rR) + Math.Abs(rB);
+ if (false == DoubleUtil.IsZero(rSpan))
+ _span = rSpan;
+ else if (0 < _dist)
+ {
+ /***
+ _nodes[count - 1] at this StylusPoint contains the length of the stroke.
+ _dist is the half peripheri of the bounding box of the stroke.
+ The idea here is that the Length/_dist is somewhat analogous to the
+ "fractal dimension" of the stroke (or in other words, how much the stroke
+ winds.)
+ Length/count is the average distance between two consequitive points
+ on the stroke. Thus, this average distance is multiplied by the winding
+ factor.
+ If the stroke were a PURE straight line across the diagone of a square,
+ Lenght/Dist will be approximately 1.41. And if there were one pixel per
+ co-ordinate, the span would have been 1.41, which works fairly well in
+ cusp detection
+ ***/
+ _span = 0.75f * (_nodes[count - 1] * _nodes[count - 1]) / (count * _dist);
+ }
+
+ if (_span < 1.0)
+ _span = 1.0f;
+
+ FindAllCusps();
+ }
+
+
+ private List _points;
+ private List _nodes;
+ private double _dist = 0;
+ private List _cusps = new List();
+
+ // Parameters governing the cusp detection algorithm
+ // Distance between probes for curvature checking
+ private double _span = 3; // Default span
+
+ struct CDataPoint
+ {
+ public Point Point; // Point (coordinates are double)
+ public int Index; // Index into the original array
+ public int TanPrev; // Previous StylusPoint Index for tangent computation
+ public int TanNext; // Next StylusPoint Index for tangent computation
+ };
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/DrawingFlags.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/DrawingFlags.cs
new file mode 100644
index 0000000..1745416
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/DrawingFlags.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ /// Flag values which can help the renderer decide how to
+ /// draw the ink strokes
+ [Flags]
+ internal enum DrawingFlags
+ {
+ /// The stroke should be drawn as a polyline
+ Polyline = 0x00000000,
+ /// The stroke should be fit to a curve, such as a bezier.
+ FitToCurve = 0x00000001,
+ /// The stroke should be rendered by subtracting its rendering values
+ /// from those on the screen
+ SubtractiveTransparency = 0x00000002,
+ /// Ignore any stylus pressure information when rendering
+ IgnorePressure = 0x00000004,
+ /// The stroke should be rendered with anti-aliased edges
+ AntiAliased = 0x00000010,
+ /// Ignore any stylus rotation information when rendering
+ IgnoreRotation = 0x00000020,
+ /// Ignore any stylus angle information when rendering
+ IgnoreAngle = 0x00000040,
+ };
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/EllipticalNodeOperations.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/EllipticalNodeOperations.cs
new file mode 100644
index 0000000..3cc918b
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/EllipticalNodeOperations.cs
@@ -0,0 +1,836 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Input;
+using System.Diagnostics;
+using WpfInk.PresentationCore.System.Windows;
+using WpfInk.PresentationCore.System.Windows.Ink;
+using WpfInk.WindowsBase.System.Windows.Media;
+
+namespace MS.Internal.Ink
+{
+ ///
+ /// StrokeNodeOperations implementation for elliptical nodes
+ ///
+ internal class EllipticalNodeOperations : StrokeNodeOperations
+ {
+ ///
+ /// Constructor
+ ///
+ ///
+ internal EllipticalNodeOperations(StylusShape nodeShape)
+ : base(nodeShape)
+ {
+ System.Diagnostics.Debug.Assert((nodeShape != null) && nodeShape.IsEllipse);
+
+ _radii = new Size(nodeShape.Width * 0.5, nodeShape.Height * 0.5);
+
+ // All operations with ellipses become simple(r) if transfrom ellipses into circles.
+ // Use the max of the radii for the radius of the circle
+ _radius = Math.Max(_radii.Width, _radii.Height);
+
+ // Compute ellipse-to-circle and circle-to-elliipse transforms. The former is used
+ // in hit-testing operations while the latter is used when computing vertices of
+ // a quadrangle connecting two ellipses
+ _transform = nodeShape.Transform;
+ _nodeShapeToCircle = _transform;
+
+ Debug.Assert(_nodeShapeToCircle.HasInverse, "About to invert a non-invertible transform");
+ _nodeShapeToCircle.Invert();
+ if (DoubleUtil.AreClose(_radii.Width, _radii.Height))
+ {
+ _circleToNodeShape = _transform;
+ }
+ else
+ {
+ // Reverse the rotation
+ if (false == DoubleUtil.IsZero(nodeShape.Rotation))
+ {
+ _nodeShapeToCircle.Rotate(-nodeShape.Rotation);
+ Debug.Assert(_nodeShapeToCircle.HasInverse, "Just rotated an invertible transform and produced a non-invertible one");
+ }
+
+ // Scale to enlarge
+ double sx, sy;
+ if (_radii.Width > _radii.Height)
+ {
+ sx = 1;
+ sy = _radii.Width / _radii.Height;
+ }
+ else
+ {
+ sx = _radii.Height / _radii.Width;
+ sy = 1;
+ }
+ _nodeShapeToCircle.Scale(sx, sy);
+ Debug.Assert(_nodeShapeToCircle.HasInverse, "Just scaled an invertible transform and produced a non-invertible one");
+
+ _circleToNodeShape = _nodeShapeToCircle;
+ _circleToNodeShape.Invert();
+ }
+ }
+
+ ///
+ /// This is probably not the best (design-wise) but the cheapest way to tell
+ /// EllipticalNodeOperations from all other implementations of node operations.
+ ///
+ internal override bool IsNodeShapeEllipse { get { return true; } }
+
+ ///
+ /// Finds connecting points for a pair of stroke nodes
+ ///
+ /// a node to connect
+ /// another node, next to beginNode
+ /// connecting quadrangle
+ internal override Quad GetConnectingQuad(in StrokeNodeData beginNode, in StrokeNodeData endNode)
+ {
+ if (beginNode.IsEmpty || endNode.IsEmpty || DoubleUtil.AreClose(beginNode.Position, endNode.Position))
+ {
+ return Quad.Empty;
+ }
+
+ // Get the vector between the node positions
+ Vector spine = endNode.Position - beginNode.Position;
+ if (_nodeShapeToCircle.IsIdentity == false)
+ {
+ spine = _nodeShapeToCircle.Transform(spine);
+ }
+
+ double beginRadius = _radius * beginNode.PressureFactor;
+ double endRadius = _radius * endNode.PressureFactor;
+
+ // Get the vector and the distance between the node positions
+ double distanceSquared = spine.LengthSquared;
+ double delta = endRadius - beginRadius;
+ double deltaSquared = DoubleUtil.IsZero(delta) ? 0 : (delta * delta);
+
+ if (DoubleUtil.LessThanOrClose(distanceSquared, deltaSquared))
+ {
+ // One circle is contained within the other
+ return Quad.Empty;
+ }
+
+ // Thus, at this point, distance > 0, which avoids the DivideByZero error
+ // Also, here, distanceSquared > deltaSquared
+ // Thus, 0 <= rSin < 1
+ // Get the components of the radius vectors
+ double distance = Math.Sqrt(distanceSquared);
+
+ spine /= distance;
+
+ Vector rad = spine;
+
+ // Turn left
+ double temp = rad.Y;
+ rad.Y = -rad.X;
+ rad.X = temp;
+
+ Vector vectorToLeftTangent, vectorToRightTangent;
+ double rSinSquared = deltaSquared / distanceSquared;
+
+ if (DoubleUtil.IsZero(rSinSquared))
+ {
+ vectorToLeftTangent = rad;
+ vectorToRightTangent = -rad;
+ }
+ else
+ {
+ rad *= Math.Sqrt(1 - rSinSquared);
+ spine *= Math.Sqrt(rSinSquared);
+ if (beginNode.PressureFactor < endNode.PressureFactor)
+ {
+ spine = -spine;
+ }
+
+ vectorToLeftTangent = spine + rad;
+ vectorToRightTangent = spine - rad;
+ }
+
+ // Get the common tangent points
+
+ if (_circleToNodeShape.IsIdentity == false)
+ {
+ vectorToLeftTangent = _circleToNodeShape.Transform(vectorToLeftTangent);
+ vectorToRightTangent = _circleToNodeShape.Transform(vectorToRightTangent);
+ }
+
+ return new Quad(beginNode.Position + (vectorToLeftTangent * beginRadius),
+ endNode.Position + (vectorToLeftTangent * endRadius),
+ endNode.Position + (vectorToRightTangent * endRadius),
+ beginNode.Position + (vectorToRightTangent * beginRadius));
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal override IEnumerable GetContourSegments(StrokeNodeData node, Quad quad)
+ {
+ System.Diagnostics.Debug.Assert(node.IsEmpty == false);
+
+ if (quad.IsEmpty)
+ {
+ Point point = node.Position;
+ point.X += _radius;
+ yield return new ContourSegment(point, point, node.Position);
+ }
+ else if (_nodeShapeToCircle.IsIdentity)
+ {
+ yield return new ContourSegment(quad.A, quad.B);
+ yield return new ContourSegment(quad.B, quad.C, node.Position);
+ yield return new ContourSegment(quad.C, quad.D);
+ yield return new ContourSegment(quad.D, quad.A);
+ }
+ }
+
+ ///
+ /// ISSUE-2004/06/15- temporary workaround to avoid hit-testing ellipses with ellipses
+ ///
+ ///
+ ///
+ ///
+ internal override IEnumerable GetNonBezierContourSegments(StrokeNodeData beginNode, StrokeNodeData endNode)
+ {
+ Quad quad = beginNode.IsEmpty ? Quad.Empty : base.GetConnectingQuad(beginNode, endNode);
+ return base.GetContourSegments(endNode, quad);
+ }
+
+
+ ///
+ /// Hit-tests a stroke segment defined by two nodes against a linear segment.
+ ///
+ /// Begin node of the stroke segment to hit-test. Can be empty (none)
+ /// End node of the stroke segment
+ /// Pre-computed quadrangle connecting the two nodes.
+ /// Can be empty if the begion node is empty or when one node is entirely inside the other
+ /// an end point of the hitting linear segment
+ /// an end point of the hitting linear segment
+ /// true if the hitting segment intersect the contour comprised of the two stroke nodes
+ internal override bool HitTest(
+ in StrokeNodeData beginNode, in StrokeNodeData endNode, Quad quad, Point hitBeginPoint, Point hitEndPoint)
+ {
+ StrokeNodeData bigNode, smallNode;
+ if (beginNode.IsEmpty || (quad.IsEmpty && (endNode.PressureFactor > beginNode.PressureFactor)))
+ {
+ // Need to test one node only
+ bigNode = endNode;
+ smallNode = StrokeNodeData.Empty;
+ }
+ else
+ {
+ // In this case the size doesn't matter.
+ bigNode = beginNode;
+ smallNode = endNode;
+ }
+
+ // Compute the positions of the involved points relative to bigNode.
+ Vector hitBegin = hitBeginPoint - bigNode.Position;
+ Vector hitEnd = hitEndPoint - bigNode.Position;
+
+ // If the node shape is an ellipse, transform the scene to turn the shape to a circle
+ if (_nodeShapeToCircle.IsIdentity == false)
+ {
+ hitBegin = _nodeShapeToCircle.Transform(hitBegin);
+ hitEnd = _nodeShapeToCircle.Transform(hitEnd);
+ }
+
+ bool isHit = false;
+
+ // Hit-test the big node
+ double bigRadius = _radius * bigNode.PressureFactor;
+ Vector nearest = GetNearest(hitBegin, hitEnd);
+ if (nearest.LengthSquared <= (bigRadius * bigRadius))
+ {
+ isHit = true;
+ }
+ else if (quad.IsEmpty == false)
+ {
+ // Hit-test the other node
+ Vector spineVector = smallNode.Position - bigNode.Position;
+ if (_nodeShapeToCircle.IsIdentity == false)
+ {
+ spineVector = _nodeShapeToCircle.Transform(spineVector);
+ }
+ double smallRadius = _radius * smallNode.PressureFactor;
+ nearest = GetNearest(hitBegin - spineVector, hitEnd - spineVector);
+ if ((nearest.LengthSquared <= (smallRadius * smallRadius)) || HitTestQuadSegment(quad, hitBeginPoint, hitEndPoint))
+ {
+ isHit = true;
+ }
+ }
+
+ return isHit;
+ }
+
+ ///
+ /// Hit-tests a stroke segment defined by two nodes against another stroke segment.
+ ///
+ /// Begin node of the stroke segment to hit-test. Can be empty (none)
+ /// End node of the stroke segment
+ /// Pre-computed quadrangle connecting the two nodes.
+ /// Can be empty if the begion node is empty or when one node is entirely inside the other
+ /// a collection of basic segments outlining the hitting contour
+ /// true if the contours intersect or overlap
+ internal override bool HitTest(
+ in StrokeNodeData beginNode, in StrokeNodeData endNode, Quad quad, IEnumerable hitContour)
+ {
+ StrokeNodeData bigNode, smallNode;
+ double bigRadiusSquared, smallRadiusSquared = 0;
+ Vector spineVector;
+ if (beginNode.IsEmpty || (quad.IsEmpty && (endNode.PressureFactor > beginNode.PressureFactor)))
+ {
+ // Need to test one node only
+ bigNode = endNode;
+ smallNode = StrokeNodeData.Empty;
+ spineVector = new Vector();
+ }
+ else
+ {
+ // In this case the size doesn't matter.
+ bigNode = beginNode;
+ smallNode = endNode;
+
+ smallRadiusSquared = _radius * smallNode.PressureFactor;
+ smallRadiusSquared *= smallRadiusSquared;
+
+ // Find position of smallNode relative to the bigNode.
+ spineVector = smallNode.Position - bigNode.Position;
+ // If the node shape is an ellipse, transform the scene to turn the shape to a circle
+ if (_nodeShapeToCircle.IsIdentity == false)
+ {
+ spineVector = _nodeShapeToCircle.Transform(spineVector);
+ }
+ }
+
+ bigRadiusSquared = _radius * bigNode.PressureFactor;
+ bigRadiusSquared *= bigRadiusSquared;
+
+ bool isHit = false;
+
+ // When hit-testing a contour against another contour, like in this case,
+ // the default implementation checks whether any edge (segment) of the hitting
+ // contour intersects with the contour of the ink segment. But this doesn't cover
+ // the case when the ink segment is entirely inside of the hitting segment.
+ // The bool variable isInside is used here to track that case. It answers the question
+ // 'Is ink contour inside if the hitting contour?'. It's initialized to 'true"
+ // and then verified for each edge of the hitting contour until there's a hit or
+ // until it's false.
+ bool isInside = true;
+
+ foreach (ContourSegment hitSegment in hitContour)
+ {
+ if (hitSegment.IsArc)
+ {
+ // ISSUE-2004/06/15-vsmirnov - ellipse vs arc hit-testing is not implemented
+ // and currently disabled in ErasingStroke
+ }
+ else
+ {
+ // Find position of the hitting segment relative to bigNode transformed to circle.
+ Vector hitBegin = hitSegment.Begin - bigNode.Position;
+ Vector hitEnd = hitBegin + hitSegment.Vector;
+ if (_nodeShapeToCircle.IsIdentity == false)
+ {
+ hitBegin = _nodeShapeToCircle.Transform(hitBegin);
+ hitEnd = _nodeShapeToCircle.Transform(hitEnd);
+ }
+
+ // Hit-test the big node
+ Vector nearest = GetNearest(hitBegin, hitEnd);
+ if (nearest.LengthSquared <= bigRadiusSquared)
+ {
+ isHit = true;
+ break;
+ }
+
+ // Hit-test the other node
+ if (quad.IsEmpty == false)
+ {
+ nearest = GetNearest(hitBegin - spineVector, hitEnd - spineVector);
+ if ((nearest.LengthSquared <= smallRadiusSquared) ||
+ HitTestQuadSegment(quad, hitSegment.Begin, hitSegment.End))
+ {
+ isHit = true;
+ break;
+ }
+ }
+
+ // While there's still a chance to find the both nodes inside the hitting contour,
+ // continue checking on position of the endNode relative to the edges of the hitting contour.
+ if (isInside &&
+ (WhereIsVectorAboutVector(endNode.Position - hitSegment.Begin, hitSegment.Vector) != HitResult.Right))
+ {
+ isInside = false;
+ }
+ }
+ }
+
+ return (isHit || isInside);
+ }
+
+ ///
+ /// Cut-test ink segment defined by two nodes and a connecting quad against a linear segment
+ ///
+ /// Begin node of the ink segment
+ /// End node of the ink segment
+ /// Pre-computed quadrangle connecting the two ink nodes
+ /// egin point of the hitting segment
+ /// End point of the hitting segment
+ /// Exact location to cut at represented by StrokeFIndices
+ internal override StrokeFIndices CutTest(
+ in StrokeNodeData beginNode, in StrokeNodeData endNode, Quad quad, Point hitBeginPoint, Point hitEndPoint)
+ {
+ // Compute the positions of the involved points relative to the endNode.
+ Vector spineVector = beginNode.IsEmpty ? new Vector(0, 0) : (beginNode.Position - endNode.Position);
+ Vector hitBegin = hitBeginPoint - endNode.Position;
+ Vector hitEnd = hitEndPoint - endNode.Position;
+
+ // If the node shape is an ellipse, transform the scene to turn the shape to a circle
+ if (_nodeShapeToCircle.IsIdentity == false)
+ {
+ spineVector = _nodeShapeToCircle.Transform(spineVector);
+ hitBegin = _nodeShapeToCircle.Transform(hitBegin);
+ hitEnd = _nodeShapeToCircle.Transform(hitEnd);
+ }
+
+ StrokeFIndices result = StrokeFIndices.Empty;
+
+ // Hit-test the end node
+ double beginRadius = 0, endRadius = _radius * endNode.PressureFactor;
+ Vector nearest = GetNearest(hitBegin, hitEnd);
+ if (nearest.LengthSquared <= (endRadius * endRadius))
+ {
+ result.EndFIndex = StrokeFIndices.AfterLast;
+ result.BeginFIndex = beginNode.IsEmpty ? StrokeFIndices.BeforeFirst : 1;
+ }
+ if (beginNode.IsEmpty == false)
+ {
+ // Hit-test the first node
+ beginRadius = _radius * beginNode.PressureFactor;
+ nearest = GetNearest(hitBegin - spineVector, hitEnd - spineVector);
+ if (nearest.LengthSquared <= (beginRadius * beginRadius))
+ {
+ result.BeginFIndex = StrokeFIndices.BeforeFirst;
+ if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ result.EndFIndex = 0;
+ }
+ }
+ }
+
+ // If both nodes are hit or nothing is hit at all, return.
+ if (result.IsFull || quad.IsEmpty
+ || (result.IsEmpty && (HitTestQuadSegment(quad, hitBeginPoint, hitEndPoint) == false)))
+ {
+ return result;
+ }
+
+ // Find out whether the {begin, end} segment intersects with the contour
+ // of the stroke segment {_lastNode, _thisNode}, and find the lower index
+ // of the fragment to cut out.
+ if (!DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst))
+ {
+ result.BeginFIndex = ClipTest(-spineVector, beginRadius, endRadius, hitBegin - spineVector, hitEnd - spineVector);
+ }
+
+ if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ result.EndFIndex = 1 - ClipTest(spineVector, endRadius, beginRadius, hitBegin, hitEnd);
+ }
+
+ if (IsInvalidCutTestResult(result))
+ {
+ return StrokeFIndices.Empty;
+ }
+
+ return result;
+ }
+
+ ///
+ /// CutTest an inking StrokeNode segment (two nodes and a connecting quadrangle) against a hitting contour
+ /// (represented by an enumerator of Contoursegments).
+ ///
+ /// The begin StrokeNodeData
+ /// The end StrokeNodeData
+ /// Connecing quadrangle between the begin and end inking node
+ /// The hitting ContourSegments
+ /// StrokeFIndices representing the location for cutting
+ internal override StrokeFIndices CutTest(
+ in StrokeNodeData beginNode, in StrokeNodeData endNode, Quad quad, IEnumerable hitContour)
+ {
+ // Compute the positions of the beginNode relative to the endNode.
+ Vector spineVector = beginNode.IsEmpty ? new Vector(0, 0) : (beginNode.Position - endNode.Position);
+ // If the node shape is an ellipse, transform the scene to turn the shape to a circle
+ if (_nodeShapeToCircle.IsIdentity == false)
+ {
+ spineVector = _nodeShapeToCircle.Transform(spineVector);
+ }
+
+ double beginRadius = 0, endRadius;
+ double beginRadiusSquared = 0, endRadiusSquared;
+
+ endRadius = _radius * endNode.PressureFactor;
+ endRadiusSquared = endRadius * endRadius;
+ if (beginNode.IsEmpty == false)
+ {
+ beginRadius = _radius * beginNode.PressureFactor;
+ beginRadiusSquared = beginRadius * beginRadius;
+ }
+
+ bool isInside = true;
+ StrokeFIndices result = StrokeFIndices.Empty;
+
+ foreach (ContourSegment hitSegment in hitContour)
+ {
+ if (hitSegment.IsArc)
+ {
+ // ISSUE-2004/06/15-vsmirnov - ellipse vs arc hit-testing is not implemented
+ // and currently disabled in ErasingStroke
+ }
+ else
+ {
+ Vector hitBegin = hitSegment.Begin - endNode.Position;
+ Vector hitEnd = hitBegin + hitSegment.Vector;
+
+ // If the node shape is an ellipse, transform the scene to turn
+ // the shape into circle.
+ if (_nodeShapeToCircle.IsIdentity == false)
+ {
+ hitBegin = _nodeShapeToCircle.Transform(hitBegin);
+ hitEnd = _nodeShapeToCircle.Transform(hitEnd);
+ }
+
+ bool isHit = false;
+
+ // Hit-test the end node
+ Vector nearest = GetNearest(hitBegin, hitEnd);
+ if (nearest.LengthSquared < endRadiusSquared)
+ {
+ isHit = true;
+ if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ result.EndFIndex = StrokeFIndices.AfterLast;
+ if (beginNode.IsEmpty)
+ {
+ result.BeginFIndex = StrokeFIndices.BeforeFirst;
+ break;
+ }
+ if (DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst))
+ {
+ break;
+ }
+ }
+ }
+
+ if ((beginNode.IsEmpty == false) && (!isHit || !DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst)))
+ {
+ // Hit-test the first node
+ nearest = GetNearest(hitBegin - spineVector, hitEnd - spineVector);
+ if (nearest.LengthSquared < beginRadiusSquared)
+ {
+ isHit = true;
+ if (!DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst))
+ {
+ result.BeginFIndex = StrokeFIndices.BeforeFirst;
+ if (DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ break;
+ }
+ }
+ }
+ }
+
+ // If both nodes are hit or nothing is hit at all, return.
+ if (beginNode.IsEmpty || (!isHit && (quad.IsEmpty ||
+ (HitTestQuadSegment(quad, hitSegment.Begin, hitSegment.End) == false))))
+ {
+ if (isInside && (WhereIsVectorAboutVector(
+ endNode.Position - hitSegment.Begin, hitSegment.Vector) != HitResult.Right))
+ {
+ isInside = false;
+ }
+ continue;
+ }
+
+ isInside = false;
+
+ // Calculate the exact locations to cut.
+ CalculateCutLocations(spineVector, hitBegin, hitEnd, endRadius, beginRadius, ref result);
+
+ if (result.IsFull)
+ {
+ break;
+ }
+ }
+ }
+
+ //
+ if (!result.IsFull)
+ {
+ if (isInside == true)
+ {
+ System.Diagnostics.Debug.Assert(result.IsEmpty);
+ result = StrokeFIndices.Full;
+ }
+ else if ((DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.BeforeFirst)) && (!DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.AfterLast)))
+ {
+ result.EndFIndex = StrokeFIndices.AfterLast;
+ }
+ else if ((DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.AfterLast)) && (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.BeforeFirst)))
+ {
+ result.BeginFIndex = StrokeFIndices.BeforeFirst;
+ }
+ }
+
+ if (IsInvalidCutTestResult(result))
+ {
+ return StrokeFIndices.Empty;
+ }
+
+ return result;
+ }
+
+ ///
+ /// Clip-Testing a circluar inking segment against a linear segment.
+ /// See http://tabletpc/longhorn/Specs/Rendering%20and%20Hit-Testing%20Ink%20in%20Avalon%20M11.doc section
+ /// 5.4.4.14 Clip-Testing a Circular Inking Segment against a Linear Segment for details of the algorithm
+ ///
+ /// Represent the spine of the inking segment pointing from the beginNode to endNode
+ /// Radius of the beginNode
+ /// Radius of the endNode
+ /// Hitting segment start point
+ /// Hitting segment end point
+ /// A double which represents the location for cutting
+ private static double ClipTest(Vector spineVector, double beginRadius, double endRadius, Vector hitBegin, Vector hitEnd)
+ {
+ // First handle the special case when the spineVector is (0,0). In other words, this is the case
+ // when the stylus stays at the the location but pressure changes.
+ if (DoubleUtil.IsZero(spineVector.X) && DoubleUtil.IsZero(spineVector.Y))
+ {
+ System.Diagnostics.Debug.Assert(DoubleUtil.AreClose(beginRadius, endRadius) == false);
+
+ Vector nearest = GetNearest(hitBegin, hitEnd);
+ double radius;
+ if (nearest.X == 0)
+ {
+ radius = Math.Abs(nearest.Y);
+ }
+ else if (nearest.Y == 0)
+ {
+ radius = Math.Abs(nearest.X);
+ }
+ else
+ {
+ radius = nearest.Length;
+ }
+ return AdjustFIndex((radius - beginRadius) / (endRadius - beginRadius));
+ }
+
+ // This change to ClipTest with a point if the two hitting segment are close enough.
+ if (DoubleUtil.AreClose(hitBegin, hitEnd))
+ {
+ return ClipTest(spineVector, beginRadius, endRadius, hitBegin);
+ }
+
+ double findex;
+ Vector hitVector = hitEnd - hitBegin;
+
+ if (DoubleUtil.IsZero(Vector.Determinant(spineVector, hitVector)))
+ {
+ // hitVector and spineVector are parallel
+ findex = ClipTest(spineVector, beginRadius, endRadius, GetNearest(hitBegin, hitEnd));
+ System.Diagnostics.Debug.Assert(!double.IsNaN(findex));
+ }
+ else
+ {
+ // On the line defined by the segment find point P1Xp, the nearest to the beginNode.Position
+ double x = GetProjectionFIndex(hitBegin, hitEnd);
+ Vector P1Xp = hitBegin + (hitVector * x);
+ if (P1Xp.LengthSquared < (beginRadius * beginRadius))
+ {
+ System.Diagnostics.Debug.Assert(DoubleUtil.IsBetweenZeroAndOne(x) == false);
+ findex = ClipTest(spineVector, beginRadius, endRadius, (0 > x) ? hitBegin : hitEnd);
+ System.Diagnostics.Debug.Assert(!double.IsNaN(findex));
+ }
+ else
+ {
+ // Find the projection point P of endNode.Position to the line (beginNode.Position, B).
+ Vector P1P2p = spineVector + GetProjection(-spineVector, P1Xp - spineVector);
+
+ //System.Diagnostics.Debug.Assert(false == DoubleUtil.IsZero(P1P2p.LengthSquared));
+ //System.Diagnostics.Debug.Assert(false == DoubleUtil.IsZero(endRadius - beginRadius + P1P2p.Length));
+ // There checks are here since if either fail no real solution can be caculated and we may
+ // as well bail out now and save the caculations that are below.
+ if (DoubleUtil.IsZero(P1P2p.LengthSquared) || DoubleUtil.IsZero(endRadius - beginRadius + P1P2p.Length))
+ return 1d;
+
+ // Calculate the findex of the point to split the ink segment at.
+ findex = (P1Xp.Length - beginRadius) / (endRadius - beginRadius + P1P2p.Length);
+ System.Diagnostics.Debug.Assert(!double.IsNaN(findex));
+
+ // Find the projection of the split point to the line of this segment.
+ Vector S = spineVector * findex;
+
+ double r = GetProjectionFIndex(hitBegin - S, hitEnd - S);
+
+ // If the nearest point misses the segment, then find the findex
+ // of the node nearest to the segment.
+ if (false == DoubleUtil.IsBetweenZeroAndOne(r))
+ {
+ findex = ClipTest(spineVector, beginRadius, endRadius, (0 > r) ? hitBegin : hitEnd);
+ System.Diagnostics.Debug.Assert(!double.IsNaN(findex));
+ }
+ }
+ }
+ return AdjustFIndex(findex);
+ }
+
+ ///
+ /// Clip-Testing a circular inking segment again a hitting point.
+ ///
+ /// What need to find out a doulbe value s, which is between 0 and 1, such that
+ /// DistanceOf(hit - s*spine) = beginRadius + s * (endRadius - beginRadius)
+ /// That is
+ /// (hit.X-s*spine.X)^2 + (hit.Y-s*spine.Y)^2 = [beginRadius + s*(endRadius-beginRadius)]^2
+ /// Rearrange
+ /// A*s^2 + B*s + C = 0
+ /// where the value of A, B and C are described in the source code.
+ /// Solving for s:
+ /// s = (-B + sqrt(B^2-4A*C))/(2A) or s = (-B - sqrt(B^2-4A*C))/(2A)
+ /// The smaller value between 0 and 1 is the one we want and discard the other one.
+ ///
+ /// Represent the spine of the inking segment pointing from the beginNode to endNode
+ /// Radius of the beginNode
+ /// Radius of the endNode
+ /// The hitting point
+ /// A double which represents the location for cutting
+ private static double ClipTest(Vector spine, double beginRadius, double endRadius, Vector hit)
+ {
+ double radDiff = endRadius - beginRadius;
+ double A = spine.X * spine.X + spine.Y * spine.Y - radDiff * radDiff;
+ double B = -2.0f * (hit.X * spine.X + hit.Y * spine.Y + beginRadius * radDiff);
+ double C = hit.X * hit.X + hit.Y * hit.Y - beginRadius * beginRadius;
+
+ // There checks are here since if either fail no real solution can be caculated and we may
+ // as well bail out now and save the caculations that are below.
+ if (DoubleUtil.IsZero(A) || !DoubleUtil.GreaterThanOrClose(B * B, 4.0f * A * C))
+ return 1d;
+
+ double tmp = Math.Sqrt(B * B - 4.0f * A * C);
+ double s1 = (-B + tmp) / (2.0f * A);
+ double s2 = (-B - tmp) / (2.0f * A);
+ double findex;
+
+ if (DoubleUtil.IsBetweenZeroAndOne(s1) && DoubleUtil.IsBetweenZeroAndOne(s1))
+ {
+ findex = Math.Min(s1, s2);
+ }
+ else if (DoubleUtil.IsBetweenZeroAndOne(s1))
+ {
+ findex = s1;
+ }
+ else if (DoubleUtil.IsBetweenZeroAndOne(s2))
+ {
+ findex = s2;
+ }
+ else
+ {
+ // There is still possiblity that value like 1.0000000000000402 is not considered
+ // as "IsOne" by DoubleUtil class. We should be at either one of the following two cases:
+ // 1. s1/s2 around 0 but not close enough (say -0.0000000000001)
+ // 2. s1/s2 around 1 but not close enought (say 1.0000000000000402)
+
+ if (s1 > 1d && s2 > 1d)
+ {
+ findex = 1d;
+ }
+ else if (s1 < 0d && s2 < 0d)
+ {
+ findex = 0d;
+ }
+ else
+ {
+ findex = Math.Abs(Math.Min(s1, s2) - 0d) < Math.Abs(Math.Max(s1, s2) - 1d) ? 0d : 1d;
+ }
+ }
+ return AdjustFIndex(findex);
+ }
+
+ ///
+ /// Helper function to find out the relative location of a segment {segBegin, segEnd} against
+ /// a strokeNode (spine).
+ ///
+ /// the spineVector of the StrokeNode
+ /// Start position of the line segment
+ /// End position of the line segment
+ /// HitResult
+ private static HitResult WhereIsNodeAboutSegment(Vector spine, Vector segBegin, Vector segEnd)
+ {
+ HitResult whereabout = HitResult.Right;
+ Vector segVector = segEnd - segBegin;
+
+ if ((WhereIsVectorAboutVector(-segBegin, segVector) == HitResult.Left)
+ && !DoubleUtil.IsZero(Vector.Determinant(spine, segVector)))
+ {
+ whereabout = HitResult.Left;
+ }
+ return whereabout;
+ }
+
+ ///
+ /// Helper method to calculate the exact location to cut
+ ///
+ /// Vector the relative location of the two inking nodes
+ /// the begin point of the hitting segment
+ /// the end point of the hitting segment
+ /// endNode radius
+ /// beginNode radius
+ /// StrokeFIndices representing the location for cutting
+ private void CalculateCutLocations(
+ Vector spineVector, Vector hitBegin, Vector hitEnd, double endRadius, double beginRadius, ref StrokeFIndices result)
+ {
+ // Find out whether the {hitBegin, hitEnd} segment intersects with the contour
+ // of the stroke segment, and find the lower index of the fragment to cut out.
+ if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ if (WhereIsNodeAboutSegment(spineVector, hitBegin, hitEnd) == HitResult.Left)
+ {
+ double findex = 1 - ClipTest(spineVector, endRadius, beginRadius, hitBegin, hitEnd);
+ if (findex > result.EndFIndex)
+ {
+ result.EndFIndex = findex;
+ }
+ }
+ }
+
+ // Find out whether the {hitBegin, hitEnd} segment intersects with the contour
+ // of the stroke segment, and find the higher index of the fragment to cut out.
+ if (!DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst))
+ {
+ hitBegin -= spineVector;
+ hitEnd -= spineVector;
+ if (WhereIsNodeAboutSegment(-spineVector, hitBegin, hitEnd) == HitResult.Left)
+ {
+ double findex = ClipTest(-spineVector, beginRadius, endRadius, hitBegin, hitEnd);
+ if (findex < result.BeginFIndex)
+ {
+ result.BeginFIndex = findex;
+ }
+ }
+ }
+ }
+
+ private double _radius = 0;
+ private Size _radii;
+ private Matrix _transform;
+ private Matrix _nodeShapeToCircle;
+ private Matrix _circleToNodeShape;
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ErasingStroke.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ErasingStroke.cs
new file mode 100644
index 0000000..e009503
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ErasingStroke.cs
@@ -0,0 +1,350 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+//#define POINTS_FILTER_TRACE
+
+using System;
+using System.Windows;
+using System.Collections.Generic;
+using WpfInk.PresentationCore.System.Windows;
+using WpfInk.PresentationCore.System.Windows.Ink;
+
+
+namespace MS.Internal.Ink
+{
+ #region ErasingStroke
+
+ ///
+ /// This class represents a contour of an erasing stroke, and provides
+ /// internal API for static and incremental stroke_contour vs stroke_contour
+ /// hit-testing.
+ ///
+ internal class ErasingStroke
+ {
+ #region Constructors
+
+ ///
+ /// Constructor for incremental erasing
+ ///
+ /// The shape of the eraser's tip
+ internal ErasingStroke(StylusShape erasingShape)
+ {
+ System.Diagnostics.Debug.Assert(erasingShape != null);
+ _nodeIterator = new StrokeNodeIterator(erasingShape);
+ }
+
+ ///
+ /// Constructor for static (atomic) erasing
+ ///
+ /// The shape of the eraser's tip
+ /// the spine of the erasing stroke
+ internal ErasingStroke(StylusShape erasingShape, IEnumerable path)
+ : this(erasingShape)
+ {
+ MoveTo(path);
+ }
+
+ #endregion
+
+ #region API
+
+ ///
+ /// Generates stroke nodes along a given path.
+ /// Drops any previously genererated nodes.
+ ///
+ ///
+ internal void MoveTo(IEnumerable path)
+ {
+ System.Diagnostics.Debug.Assert((path != null) && (IEnumerablePointHelper.GetCount(path) != 0));
+ Point[] points = IEnumerablePointHelper.GetPointArray(path);
+
+ if (_erasingStrokeNodes == null)
+ {
+ _erasingStrokeNodes = new List(points.Length);
+ }
+ else
+ {
+ _erasingStrokeNodes.Clear();
+ }
+
+
+ _bounds = Rect.Empty;
+ _nodeIterator = _nodeIterator.GetIteratorForNextSegment(points.Length > 1 ? FilterPoints(points) : points);
+ for (int i = 0; i < _nodeIterator.Count; i++)
+ {
+ StrokeNode strokeNode = _nodeIterator[i];
+ _bounds.Union(strokeNode.GetBoundsConnected());
+ _erasingStrokeNodes.Add(strokeNode);
+ }
+#if POINTS_FILTER_TRACE
+ _totalPointsAdded += path.Length;
+ System.Diagnostics.Debug.WriteLine(String.Format("Total Points added: {0} screened: {1} collinear screened: {2}", _totalPointsAdded, _totalPointsScreened, _collinearPointsScreened));
+#endif
+
+ }
+
+ ///
+ /// Returns the bounds of the eraser's last move.
+ ///
+ ///
+ internal Rect Bounds { get { return _bounds; } }
+
+ ///
+ /// Hit-testing for stroke erase scenario.
+ ///
+ /// the stroke nodes to iterate
+ /// true if the strokes intersect, false otherwise
+ internal bool HitTest(StrokeNodeIterator iterator)
+ {
+ System.Diagnostics.Debug.Assert(iterator != null);
+
+ if ((_erasingStrokeNodes == null) || (_erasingStrokeNodes.Count == 0))
+ {
+ return false;
+ }
+
+ Rect inkSegmentBounds = Rect.Empty;
+ for (int i = 0; i < iterator.Count; i++)
+ {
+ StrokeNode inkStrokeNode = iterator[i];
+ Rect inkNodeBounds = inkStrokeNode.GetBounds();
+ inkSegmentBounds.Union(inkNodeBounds);
+
+ if (inkSegmentBounds.IntersectsWith(_bounds))
+ {
+ // can be optimized (using pre-computed bounds
+ // of parts of the erasing stroke)
+ foreach (StrokeNode erasingStrokeNode in _erasingStrokeNodes)
+ {
+ if (inkSegmentBounds.IntersectsWith(erasingStrokeNode.GetBoundsConnected())
+ && erasingStrokeNode.HitTest(inkStrokeNode))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// Hit-testing for point erase.
+ ///
+ ///
+ ///
+ ///
+ internal bool EraseTest(StrokeNodeIterator iterator, List intersections)
+ {
+ System.Diagnostics.Debug.Assert(iterator != null);
+ System.Diagnostics.Debug.Assert(intersections != null);
+ intersections.Clear();
+
+ List eraseAt = new List();
+
+ if ((_erasingStrokeNodes == null) || (_erasingStrokeNodes.Count == 0))
+ {
+ return false;
+ }
+
+ Rect inkSegmentBounds = Rect.Empty;
+ for (int x = 0; x < iterator.Count; x++)
+ {
+ StrokeNode inkStrokeNode = iterator[x];
+ Rect inkNodeBounds = inkStrokeNode.GetBounds();
+ inkSegmentBounds.Union(inkNodeBounds);
+
+ if (inkSegmentBounds.IntersectsWith(_bounds))
+ {
+ // can be optimized (using pre-computed bounds
+ // of parts of the erasing stroke)
+ int index = eraseAt.Count;
+ foreach (StrokeNode erasingStrokeNode in _erasingStrokeNodes)
+ {
+ if (false == inkSegmentBounds.IntersectsWith(erasingStrokeNode.GetBoundsConnected()))
+ {
+ continue;
+ }
+
+ StrokeFIndices fragment = inkStrokeNode.CutTest(erasingStrokeNode);
+ if (fragment.IsEmpty)
+ {
+ continue;
+ }
+
+ // Merge it with the other results for this ink segment
+ bool inserted = false;
+ for (int i = index; i < eraseAt.Count; i++)
+ {
+ StrokeFIndices lastFragment = eraseAt[i];
+ if (fragment.BeginFIndex < lastFragment.EndFIndex)
+ {
+ // If the fragments overlap, merge them
+ if (fragment.EndFIndex > lastFragment.BeginFIndex)
+ {
+ fragment = new StrokeFIndices(
+ Math.Min(lastFragment.BeginFIndex, fragment.BeginFIndex),
+ Math.Max(lastFragment.EndFIndex, fragment.EndFIndex));
+
+ // If the fragment doesn't go beyond lastFragment, break
+ if ((fragment.EndFIndex <= lastFragment.EndFIndex) || ((i + 1) == eraseAt.Count))
+ {
+ inserted = true;
+ eraseAt[i] = fragment;
+ break;
+ }
+ else
+ {
+ eraseAt.RemoveAt(i);
+ i--;
+ }
+ }
+ // insert otherwise
+ else
+ {
+ eraseAt.Insert(i, fragment);
+ inserted = true;
+ break;
+ }
+ }
+ }
+
+ // If not merged nor inserted, add it to the end of the list
+ if (false == inserted)
+ {
+ eraseAt.Add(fragment);
+ }
+ // Break out if the entire ink segment is hit - {BeforeFirst, AfterLast}
+ if (eraseAt[eraseAt.Count - 1].IsFull)
+ {
+ break;
+ }
+ }
+ // Merge inter-segment overlapping fragments
+ if ((index > 0) && (index < eraseAt.Count))
+ {
+ StrokeFIndices lastFragment = eraseAt[index - 1];
+ if (DoubleUtil.AreClose(lastFragment.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ if (DoubleUtil.AreClose(eraseAt[index].BeginFIndex, StrokeFIndices.BeforeFirst))
+ {
+ lastFragment.EndFIndex = eraseAt[index].EndFIndex;
+ eraseAt[index - 1] = lastFragment;
+ eraseAt.RemoveAt(index);
+ }
+ else
+ {
+ lastFragment.EndFIndex = inkStrokeNode.Index;
+ eraseAt[index - 1] = lastFragment;
+ }
+ }
+ }
+ }
+ // Start next ink segment
+ inkSegmentBounds = inkNodeBounds;
+ }
+ if (eraseAt.Count != 0)
+ {
+ foreach (StrokeFIndices segment in eraseAt)
+ {
+ intersections.Add(new StrokeIntersection(segment.BeginFIndex, StrokeFIndices.AfterLast,
+ StrokeFIndices.BeforeFirst, segment.EndFIndex));
+ }
+ }
+ return (eraseAt.Count != 0);
+ }
+
+ #endregion
+
+ #region private API
+ private Point[] FilterPoints(Point[] path)
+ {
+ System.Diagnostics.Debug.Assert(path.Length > 1);
+ Point back2, back1;
+ int i;
+ List newPath = new List();
+ if (_nodeIterator.Count == 0)
+ {
+ newPath.Add(path[0]);
+ newPath.Add(path[1]);
+ back2 = path[0];
+ back1 = path[1];
+ i = 2;
+ }
+ else
+ {
+ newPath.Add(path[0]);
+ back2 = _nodeIterator[_nodeIterator.Count - 1].Position;
+ back1 = path[0];
+ i = 1;
+ }
+
+ while (i < path.Length)
+ {
+ if (DoubleUtil.AreClose(back1, path[i]))
+ {
+ // Filter out duplicate points
+ i++;
+ continue;
+ }
+
+ Vector begin = back2 - back1;
+ Vector end = path[i] - back1;
+ //On a line defined by begin & end, finds the findex of the point nearest to the origin (0,0).
+ double findex = StrokeNodeOperations.GetProjectionFIndex(begin, end);
+
+ if (DoubleUtil.IsBetweenZeroAndOne(findex))
+ {
+ Vector v = (begin + (end - begin) * findex);
+ if (v.LengthSquared < CollinearTolerance)
+ {
+ // The point back1 can be considered as on the line from back2 to the toTest StrokeNode.
+ // Modify the previous point.
+ newPath[newPath.Count - 1] = path[i];
+ back1 = path[i];
+ i++;
+#if POINTS_FILTER_TRACE
+ _collinearPointsScreened ++;
+#endif
+ continue;
+ }
+ }
+
+ // Add the surviving point into the list.
+ newPath.Add(path[i]);
+ back2 = back1;
+ back1 = path[i];
+ i++;
+ }
+#if POINTS_FILTER_TRACE
+ _totalPointsScreened += path.Length - newPath.Count;
+#endif
+ return newPath.ToArray();
+ }
+
+ #endregion
+
+ #region Fields
+
+ private StrokeNodeIterator _nodeIterator;
+ private List _erasingStrokeNodes = null;
+ private Rect _bounds = Rect.Empty;
+
+#if POINTS_FILTER_TRACE
+ private int _totalPointsAdded = 0;
+ private int _totalPointsScreened = 0;
+ private int _collinearPointsScreened = 0;
+#endif
+
+ // The collinear tolerance used in points filtering algorithm. The valie
+ // should be further tuned considering trade-off of performance and accuracy.
+ // In general, the larger the value, more points are filtered but less accurate.
+ // For a value of 0.5, typically 70% - 80% percent of the points are filtered out.
+ private static readonly double CollinearTolerance = 0.1f;
+
+ #endregion
+ }
+
+ #endregion
+}
+
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ExtendedProperty.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ExtendedProperty.cs
new file mode 100644
index 0000000..ef85c93
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ExtendedProperty.cs
@@ -0,0 +1,214 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using SRID = MS.Internal.PresentationCore.SRID;
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ ///
+ /// Drawing Attribute Key/Value pair for specifying each attribute
+ ///
+ internal sealed class ExtendedProperty
+ {
+ ///
+ /// Create a new drawing attribute with the specified key and value
+ ///
+ /// Identifier of attribute
+ /// Attribute value - not that the Type for value is tied to the id
+ /// Value type must be compatible with attribute Id
+ internal ExtendedProperty(Guid id, object value)
+ {
+ if (id == Guid.Empty)
+ {
+ throw new ArgumentException(SR.Get(SRID.InvalidGuid));
+ }
+ _id = id;
+ Value = value;
+ }
+
+ /// Returns a value that can be used to store and lookup
+ /// ExtendedProperty objects in a hash table
+ public override int GetHashCode()
+ {
+ return Id.GetHashCode() ^ Value.GetHashCode();
+ }
+
+ /// Determine if two ExtendedProperty objects are equal
+ public override bool Equals(object obj)
+ {
+ if (obj == null || obj.GetType() != GetType())
+ {
+ return false;
+ }
+
+ ExtendedProperty that = (ExtendedProperty) obj;
+
+ if (that.Id == this.Id)
+ {
+ Type type1 = this.Value.GetType();
+ Type type2 = that.Value.GetType();
+
+ if (type1.IsArray && type2.IsArray)
+ {
+ Type elementType1 = type1.GetElementType();
+ Type elementType2 = type2.GetElementType();
+ if (elementType1 == elementType2 &&
+ elementType1.IsValueType &&
+ type1.GetArrayRank() == 1 &&
+ elementType2.IsValueType &&
+ type2.GetArrayRank() == 1)
+ {
+ Array array1 = (Array) this.Value;
+ Array array2 = (Array) that.Value;
+ if (array1.Length == array2.Length)
+ {
+ for (int i = 0; i < array1.Length; i++)
+ {
+ if (!array1.GetValue(i).Equals(array2.GetValue(i)))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ }
+ else
+ {
+ return that.Value.Equals(this.Value);
+ }
+ }
+ return false;
+ }
+
+ /// Overload of the equality operator for comparing
+ /// two ExtendedProperty objects
+ public static bool operator ==(ExtendedProperty first, ExtendedProperty second)
+ {
+ if ((object) first == null && (object) second == null)
+ {
+ return true;
+ }
+ else if ((object) first == null || (object) second == null)
+ {
+ return false;
+ }
+ else
+ {
+ return first.Equals(second);
+ }
+ }
+
+ /// Compare two custom attributes for Id and value inequality
+ /// Value comparison is performed based on Value.Equals
+ public static bool operator !=(ExtendedProperty first, ExtendedProperty second)
+ {
+ return !(first == second);
+ }
+
+ ///
+ /// Returns a debugger-friendly version of the ExtendedProperty
+ ///
+ ///
+ public override string ToString()
+ {
+ string val;
+ if (Value == null)
+ {
+ val = "";
+ }
+ else if (Value is string)
+ {
+ val = "\"" + Value.ToString() + "\"";
+ }
+ else
+ {
+ val = Value.ToString();
+ }
+ return Id + "," + val;
+ }
+
+ ///
+ /// Retrieve the Identifier, or key, for Drawing Attribute key/value pair
+ ///
+ internal Guid Id
+ {
+ get
+ {
+ return _id;
+ }
+ }
+ ///
+ /// Set or retrieve the value for ExtendedProperty key/value pair
+ ///
+ /// Value type must be compatible with attribute Id
+ /// Value can be null.
+ internal object Value
+ {
+ get
+ {
+ return _value;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ _value = value;
+ }
+ }
+
+ /////
+ ///// Creates a copy of the Guid and Value
+ /////
+ /////
+ //internal ExtendedProperty Clone()
+ //{
+ // //
+ // // the only properties we accept are value types or arrays of
+ // // value types with the exception of string.
+ // //
+ // Guid guid = _id; //Guid is a ValueType that copies on assignment
+ // Type type = _value.GetType();
+
+ // //
+ // // check for the very common, copy on assignment
+ // // types (ValueType or string)
+ // //
+ // if (type.IsValueType || type == typeof(string))
+ // {
+ // //
+ // // either ValueType or string is passed by value
+ // //
+ // return new ExtendedProperty(guid, _value);
+ // }
+ // else if (type.IsArray)
+ // {
+ // Type elementType = type.GetElementType();
+ // if (elementType.IsValueType && type.GetArrayRank() == 1)
+ // {
+ // //
+ // // copy the array memebers, which we know are copy
+ // // on assignment value types
+ // //
+ // Array newArray = Array.CreateInstance(elementType, ((Array) _value).Length);
+ // Array.Copy((Array) _value, newArray, ((Array) _value).Length);
+ // return new ExtendedProperty(guid, newArray);
+ // }
+ // }
+ // //
+ // // we didn't find a type we expect, throw
+ // //
+ // throw new InvalidOperationException(SR.Get(SRID.InvalidDataTypeForExtendedProperty));
+ //}
+
+
+ private Guid _id; // id of attribute
+ private object _value; // data in attribute
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ExtendedPropertyCollection.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ExtendedPropertyCollection.cs
new file mode 100644
index 0000000..fbec81c
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/ExtendedPropertyCollection.cs
@@ -0,0 +1,372 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+
+using SRID = MS.Internal.PresentationCore.SRID;
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ ///
+ /// A collection of name/value pairs, called ExtendedProperties, can be stored
+ /// in a collection to enable aggregate operations and assignment to Ink object
+ /// model objects, such StrokeCollection and Stroke.
+ ///
+ internal sealed class ExtendedPropertyCollection //does not implement ICollection, we don't need it
+ {
+ ///
+ /// Create a new empty ExtendedPropertyCollection
+ ///
+ internal ExtendedPropertyCollection()
+ {
+ }
+
+ /// Overload of the Equals method which determines if two ExtendedPropertyCollection
+ /// objects contain equivalent key/value pairs
+ public override bool Equals(object o)
+ {
+ if (o == null || o.GetType() != GetType())
+ {
+ return false;
+ }
+
+ //
+ // compare counts
+ //
+ ExtendedPropertyCollection that = (ExtendedPropertyCollection) o;
+ if (that.Count != this.Count)
+ {
+ return false;
+ }
+
+ //
+ // counts are equal, compare individual items
+ //
+ //
+ for (int x = 0; x < that.Count; x++)
+ {
+ bool cont = false;
+ for (int i = 0; i < _extendedProperties.Count; i++)
+ {
+ if (_extendedProperties[i].Equals(that[x]))
+ {
+ cont = true;
+ break;
+ }
+ }
+ if (!cont)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /// Overload of the equality operator which determines
+ /// if two ExtendedPropertyCollections are equal
+ public static bool operator ==(ExtendedPropertyCollection first, ExtendedPropertyCollection second)
+ {
+ // compare the GC ptrs for the obvious reference equality
+ if (((object) first == null && (object) second == null) ||
+ ((object) first == (object) second))
+ {
+ return true;
+ }
+ // otherwise, if one of the ptrs are null, but not the other then return false
+ else if ((object) first == null || (object) second == null)
+ {
+ return false;
+ }
+ // finally use the full `blown value-style comparison against the collection contents
+ else
+ {
+ return first.Equals(second);
+ }
+ }
+
+ /// Overload of the not equals operator to determine if two
+ /// ExtendedPropertyCollections have different key/value pairs
+ public static bool operator !=(ExtendedPropertyCollection first, ExtendedPropertyCollection second)
+ {
+ return !(first == second);
+ }
+
+ ///
+ /// GetHashCode
+ ///
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+
+ ///
+ /// Check to see if the attribute is defined in the collection.
+ ///
+ /// Attribute identifier
+ /// True if attribute is set in the mask, false otherwise
+ internal bool Contains(Guid attributeId)
+ {
+ for (int x = 0; x < _extendedProperties.Count; x++)
+ {
+ if (_extendedProperties[x].Id == attributeId)
+ {
+ //
+ // a typical pattern is to first check if
+ // ep.Contains(guid)
+ // before accessing:
+ // object o = ep[guid];
+ //
+ // I'm caching the index that contains returns so that we
+ // can look there first for the guid in the indexer
+ //
+ _optimisticIndex = x;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /////
+ ///// Copies the ExtendedPropertyCollection
+ /////
+ ///// Copy of the ExtendedPropertyCollection
+ ///// Any reference types held in the collection will only be deep copied (e.g. Arrays).
+ /////
+ //internal ExtendedPropertyCollection Clone()
+ //{
+ // ExtendedPropertyCollection copied = new ExtendedPropertyCollection();
+ // for (int x = 0; x < _extendedProperties.Count; x++)
+ // {
+ // copied.Add(_extendedProperties[x].Clone());
+ // }
+ // return copied;
+ //}
+
+ ///
+ /// Add
+ ///
+ /// Id
+ /// value
+ internal void Add(Guid id, object value)
+ {
+ if (this.Contains(id))
+ {
+ throw new ArgumentException(SR.Get(SRID.EPExists), "id");
+ }
+
+ ExtendedProperty extendedProperty = new ExtendedProperty(id, value);
+
+ //this will raise change events
+ this.Add(extendedProperty);
+ }
+
+
+ ///
+ /// Remove
+ ///
+ /// id
+ internal void Remove(Guid id)
+ {
+ if (!Contains(id))
+ {
+ throw new ArgumentException(SR.Get(SRID.EPGuidNotFound), "id");
+ }
+
+ ExtendedProperty propertyToRemove = GetExtendedPropertyById(id);
+ Debug.Assert(propertyToRemove != null);
+
+ _extendedProperties.Remove(propertyToRemove);
+
+ //
+ // this value is bogus now
+ //
+ _optimisticIndex = -1;
+
+ // fire notification event
+ if (this.Changed != null)
+ {
+ ExtendedPropertiesChangedEventArgs eventArgs
+ = new ExtendedPropertiesChangedEventArgs(propertyToRemove, null);
+ this.Changed(this, eventArgs);
+ }
+ }
+
+ ///
+ /// Retrieve the Guid array of ExtendedProperty Ids in the collection.
+ /// Guid[] is of type .
+ ///
+ ///
+ internal Guid[] GetGuidArray()
+ {
+ if (_extendedProperties.Count > 0)
+ {
+ Guid[] guids = new Guid[_extendedProperties.Count];
+ for (int i = 0; i < _extendedProperties.Count; i++)
+ {
+ guids[i] = this[i].Id;
+ }
+ return guids;
+ }
+ else
+ {
+ return Array.Empty();
+ }
+ }
+
+ ///
+ /// Generic accessor for the ExtendedPropertyCollection.
+ ///
+ /// Attribue Id to find
+ /// Value for attribute specified by Id
+ /// Specified identifier was not found
+ ///
+ /// Note that you can access extended properties via this indexer.
+ ///
+ internal object this[Guid attributeId]
+ {
+ get
+ {
+ ExtendedProperty ep = GetExtendedPropertyById(attributeId);
+ if (ep == null)
+ {
+ throw new ArgumentException(SR.Get(SRID.EPNotFound), "attributeId");
+ }
+ return ep.Value;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+ for (int i = 0; i < _extendedProperties.Count; i++)
+ {
+ ExtendedProperty currentProperty = _extendedProperties[i];
+
+ if (currentProperty.Id == attributeId)
+ {
+ object oldValue = currentProperty.Value;
+ //this will raise events
+ currentProperty.Value = value;
+
+ //raise change if anyone is listening
+ if (this.Changed != null)
+ {
+ ExtendedPropertiesChangedEventArgs eventArgs
+ = new ExtendedPropertiesChangedEventArgs(
+ new ExtendedProperty(currentProperty.Id, oldValue), //old prop
+ currentProperty); //new prop
+
+ this.Changed(this, eventArgs);
+ }
+ return;
+ }
+ }
+
+ //
+ // we didn't find the Id in the collection, we need to add it.
+ // this will raise change notifications
+ //
+ ExtendedProperty attributeToAdd = new ExtendedProperty(attributeId, value);
+ this.Add(attributeToAdd);
+ }
+ }
+
+ ///
+ /// Generic accessor for the ExtendedPropertyCollection.
+ ///
+ /// index into masking collection to retrieve
+ /// ExtendedProperty specified at index
+ /// Index was not found
+ ///
+ /// Note that you can access extended properties via this indexer.
+ ///
+ internal ExtendedProperty this[int index]
+ {
+ get
+ {
+ return _extendedProperties[index];
+ }
+ }
+
+ ///
+ /// Retrieve the number of ExtendedProperty objects in the collection.
+ /// Count is of type .
+ ///
+ ///
+ internal int Count
+ {
+ get
+ {
+ return _extendedProperties.Count;
+ }
+ }
+
+ ///
+ /// Event fired whenever a ExtendedProperty is modified in the collection
+ ///
+ internal event ExtendedPropertiesChangedEventHandler Changed;
+
+
+ ///
+ /// private Add, we need to consider making this public in order to implement the generic ICollection
+ ///
+ private void Add(ExtendedProperty extendedProperty)
+ {
+ Debug.Assert(!this.Contains(extendedProperty.Id), "ExtendedProperty already belongs to the collection");
+
+ _extendedProperties.Add(extendedProperty);
+
+ // fire notification event
+ if (this.Changed != null)
+ {
+ ExtendedPropertiesChangedEventArgs eventArgs
+ = new ExtendedPropertiesChangedEventArgs(null, extendedProperty);
+ this.Changed(this, eventArgs);
+ }
+ }
+
+ ///
+ /// Private helper for getting an EP out of our internal collection
+ ///
+ /// id
+ private ExtendedProperty GetExtendedPropertyById(Guid id)
+ {
+ //
+ // a typical pattern is to first check if
+ // ep.Contains(guid)
+ // before accessing:
+ // object o = ep[guid];
+ //
+ // The last call to .Contains sets this index
+ //
+ if (_optimisticIndex != -1 &&
+ _optimisticIndex < _extendedProperties.Count &&
+ _extendedProperties[_optimisticIndex].Id == id)
+ {
+ return _extendedProperties[_optimisticIndex];
+ }
+
+ //we didn't find the ep optimistically, perform linear lookup
+ for (int i = 0; i < _extendedProperties.Count; i++)
+ {
+ if (_extendedProperties[i].Id == id)
+ {
+ return _extendedProperties[i];
+ }
+ }
+
+ return null;
+ }
+
+ // the set of ExtendedProperties stored in this collection
+ private List _extendedProperties = new List();
+
+
+ //used to optimize across Contains / Index calls
+ private int _optimisticIndex = -1;
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/InkSerializedFormat/ISFTagAndGuidCache.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/InkSerializedFormat/ISFTagAndGuidCache.cs
new file mode 100644
index 0000000..71b25b8
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/InkSerializedFormat/ISFTagAndGuidCache.cs
@@ -0,0 +1,154 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections;
+using System.IO;
+
+namespace MS.Internal.Ink.InkSerializedFormat
+{
+ ///
+ /// [To be supplied.]
+ ///
+ internal static class KnownIdCache
+ {
+ // This id table includes the original Guids that were hardcoded
+ // into ISF for the TabletPC v1 release
+ public static Guid[] OriginalISFIdTable = {
+ new Guid(0x598a6a8f, 0x52c0, 0x4ba0, 0x93, 0xaf, 0xaf, 0x35, 0x74, 0x11, 0xa5, 0x61),
+ new Guid(0xb53f9f75, 0x04e0, 0x4498, 0xa7, 0xee, 0xc3, 0x0d, 0xbb, 0x5a, 0x90, 0x11),
+ new Guid(0x735adb30, 0x0ebb, 0x4788, 0xa0, 0xe4, 0x0f, 0x31, 0x64, 0x90, 0x05, 0x5d),
+ new Guid(0x6e0e07bf, 0xafe7, 0x4cf7, 0x87, 0xd1, 0xaf, 0x64, 0x46, 0x20, 0x84, 0x18),
+ new Guid(0x436510c5, 0xfed3, 0x45d1, 0x8b, 0x76, 0x71, 0xd3, 0xea, 0x7a, 0x82, 0x9d),
+ new Guid(0x78a81b56, 0x0935, 0x4493, 0xba, 0xae, 0x00, 0x54, 0x1a, 0x8a, 0x16, 0xc4),
+ new Guid(0x7307502d, 0xf9f4, 0x4e18, 0xb3, 0xf2, 0x2c, 0xe1, 0xb1, 0xa3, 0x61, 0x0c),
+ new Guid(0x6da4488b, 0x5244, 0x41ec, 0x90, 0x5b, 0x32, 0xd8, 0x9a, 0xb8, 0x08, 0x09),
+ new Guid(0x8b7fefc4, 0x96aa, 0x4bfe, 0xac, 0x26, 0x8a, 0x5f, 0x0b, 0xe0, 0x7b, 0xf5),
+ new Guid(0xa8d07b3a, 0x8bf0, 0x40b0, 0x95, 0xa9, 0xb8, 0x0a, 0x6b, 0xb7, 0x87, 0xbf),
+ new Guid(0x0e932389, 0x1d77, 0x43af, 0xac, 0x00, 0x5b, 0x95, 0x0d, 0x6d, 0x4b, 0x2d),
+ new Guid(0x029123b4, 0x8828, 0x410b, 0xb2, 0x50, 0xa0, 0x53, 0x65, 0x95, 0xe5, 0xdc),
+ new Guid(0x82dec5c7, 0xf6ba, 0x4906, 0x89, 0x4f, 0x66, 0xd6, 0x8d, 0xfc, 0x45, 0x6c),
+ new Guid(0x0d324960, 0x13b2, 0x41e4, 0xac, 0xe6, 0x7a, 0xe9, 0xd4, 0x3d, 0x2d, 0x3b),
+ new Guid(0x7f7e57b7, 0xbe37, 0x4be1, 0xa3, 0x56, 0x7a, 0x84, 0x16, 0x0e, 0x18, 0x93),
+ new Guid(0x5d5d5e56, 0x6ba9, 0x4c5b, 0x9f, 0xb0, 0x85, 0x1c, 0x91, 0x71, 0x4e, 0x56),
+ new Guid(0x6a849980, 0x7c3a, 0x45b7, 0xaa, 0x82, 0x90, 0xa2, 0x62, 0x95, 0x0e, 0x89),
+ new Guid(0x33c1df83, 0xecdb, 0x44f0, 0xb9, 0x23, 0xdb, 0xd1, 0xa5, 0xb2, 0x13, 0x6e),
+ new Guid(0x5329cda5, 0xfa5b, 0x4ed2, 0xbb, 0x32, 0x83, 0x46, 0x01, 0x72, 0x44, 0x28),
+ new Guid(0x002df9af, 0xdd8c, 0x4949, 0xba, 0x46, 0xd6, 0x5e, 0x10, 0x7d, 0x1a, 0x8a),
+ new Guid(0x9d32b7ca, 0x1213, 0x4f54, 0xb7, 0xe4, 0xc9, 0x05, 0x0e, 0xe1, 0x7a, 0x38),
+ new Guid(0xe71caab9, 0x8059, 0x4c0d, 0xa2, 0xdb, 0x7c, 0x79, 0x54, 0x47, 0x8d, 0x82),
+ new Guid(0x5c0b730a, 0xf394, 0x4961, 0xa9, 0x33, 0x37, 0xc4, 0x34, 0xf4, 0xb7, 0xeb),
+ new Guid(0x2812210f, 0x871e, 0x4d91, 0x86, 0x07, 0x49, 0x32, 0x7d, 0xdf, 0x0a, 0x9f),
+ new Guid(0x8359a0fa, 0x2f44, 0x4de6, 0x92, 0x81, 0xce, 0x5a, 0x89, 0x9c, 0xf5, 0x8f),
+ new Guid(0x4c4642dd, 0x479e, 0x4c66, 0xb4, 0x40, 0x1f, 0xcd, 0x83, 0x95, 0x8f, 0x00),
+ new Guid(0xce2d9a8a, 0xe58e, 0x40ba, 0x93, 0xfa, 0x18, 0x9b, 0xb3, 0x90, 0x00, 0xae),
+ new Guid(0xc3c7480f, 0x5839, 0x46ef, 0xa5, 0x66, 0xd8, 0x48, 0x1c, 0x7a, 0xfe, 0xc1),
+ new Guid(0xea2278af, 0xc59d, 0x4ef4, 0x98, 0x5b, 0xd4, 0xbe, 0x12, 0xdf, 0x22, 0x34),
+ new Guid(0xb8630dc9, 0xcc5c, 0x4c33, 0x8d, 0xad, 0xb4, 0x7f, 0x62, 0x2b, 0x8c, 0x79),
+ new Guid(0x15e2f8e6, 0x6381, 0x4e8b, 0xa9, 0x65, 0x01, 0x1f, 0x7d, 0x7f, 0xca, 0x38),
+ new Guid(0x7066fbe4, 0x473e, 0x4675, 0x9c, 0x25, 0x00, 0x26, 0x82, 0x9b, 0x40, 0x1f),
+ new Guid(0xbbc85b9a, 0xade6, 0x4093, 0xb3, 0xbb, 0x64, 0x1f, 0xa1, 0xd3, 0x7a, 0x1a),
+ new Guid(0x39143d3, 0x78cb, 0x449c, 0xa8, 0xe7, 0x67, 0xd1, 0x88, 0x64, 0xc3, 0x32),
+ new Guid(0x67743782, 0xee5, 0x419a, 0xa1, 0x2b, 0x27, 0x3a, 0x9e, 0xc0, 0x8f, 0x3d),
+ new Guid(0xf0720328, 0x663b, 0x418f, 0x85, 0xa6, 0x95, 0x31, 0xae, 0x3e, 0xcd, 0xfa),
+ new Guid(0xa1718cdd, 0xdac, 0x4095, 0xa1, 0x81, 0x7b, 0x59, 0xcb, 0x10, 0x6b, 0xfb),
+ new Guid(0x810a74d2, 0x6ee2, 0x4e39, 0x82, 0x5e, 0x6d, 0xef, 0x82, 0x6a, 0xff, 0xc5),
+ };
+
+ // Size of data used by identified by specified Guid/Id
+
+ public enum OriginalISFIdIndex : uint
+ {
+ X = 0,
+ Y = 1,
+ Z = 2,
+ PacketStatus = 3,
+ TimerTick = 4,
+ SerialNumber = 5,
+ NormalPressure = 6,
+ TangentPressure = 7,
+ ButtonPressure = 8,
+ XTiltOrientation = 9,
+ YTiltOrientation = 10,
+ AzimuthOrientation = 11,
+ AltitudeOrientation = 12,
+ TwistOrientation = 13,
+ PitchRotation = 14,
+ RollRotation = 15,
+ YawRotation = 16,
+ PenStyle = 17,
+ ColorRef = 18,
+ StylusWidth = 19,
+ StylusHeight = 20,
+ PenTip = 21,
+ DrawingFlags = 22,
+ CursorId = 23,
+ WordAlternates = 24,
+ CharAlternates = 25,
+ InkMetrics = 26,
+ GuideStructure = 27,
+ Timestamp = 28,
+ Language = 29,
+ Transparency = 30,
+ CurveFittingError = 31,
+ RecoLattice = 32,
+ CursorDown = 33,
+ SecondaryTipSwitch = 34,
+ BarrelDown = 35,
+ TabletPick = 36,
+ RasterOperation = 37,
+ MAXIMUM = 37,
+ }
+
+ // This id table includes the Guids that used the internal persistence APIs
+ // - meaning they didn't have the data type information encoded in ISF
+ public static Guid[] TabletInternalIdTable = {
+ // Highlighter
+ new Guid(0x9b6267b8, 0x3968, 0x4048, 0xab, 0x74, 0xf4, 0x90, 0x40, 0x6a, 0x2d, 0xfa),
+ // Ink properties
+ new Guid(0x7fc30e91, 0xd68d, 0x4f07, 0x8b, 0x62, 0x6, 0xf6, 0xd2, 0x73, 0x1b, 0xed),
+ // Ink Style Bold
+ new Guid(0xe02fb5c1, 0x9693, 0x4312, 0xa4, 0x34, 0x0, 0xde, 0x7f, 0x3a, 0xd4, 0x93),
+ // Ink Style Italics
+ new Guid(0x5253b51, 0x49c6, 0x4a04, 0x89, 0x93, 0x64, 0xdd, 0x9a, 0xbd, 0x84, 0x2a),
+ // Stroke Timestamp
+ new Guid(0x4ea66c4, 0xf33a, 0x461b, 0xb8, 0xfe, 0x68, 0x7, 0xd, 0x9c, 0x75, 0x75),
+ // Stroke Time Id
+ new Guid(0x50b6bc8, 0x3b7d, 0x4816, 0x8c, 0x61, 0xbc, 0x7e, 0x90, 0x5b, 0x21, 0x32),
+ // Stroke Lattice
+ new Guid(0x82871c85, 0xe247, 0x4d8c, 0x8d, 0x71, 0x22, 0xe5, 0xd6, 0xf2, 0x57, 0x76),
+ // Ink Custom Strokes
+ new Guid(0x33cdbbb3, 0x588f, 0x4e94, 0xb1, 0xfe, 0x5d, 0x79, 0xff, 0xe7, 0x6e, 0x76),
+ };
+ // lookup indices for table of GUIDs used with non-Automation APIs
+ internal enum TabletInternalIdIndex
+ {
+ Highlighter = 0,
+ InkProperties = 1,
+ InkStyleBold = 2,
+ InkStyleItalics = 3,
+ StrokeTimestamp = 4,
+ StrokeTimeId = 5,
+ InkStrokeLattice = 6,
+ InkCustomStrokes = 7,
+ MAXIMUM = 7
+ }
+
+ // The maximum value that can be encoded into a single byte is 127.
+ // To improve the chances of storing all of the guids in the ISF guid table
+ // with single-byte lookups, the guids are broken into two ranges
+ // 0-50 known tags
+ // 50-100 known guids (reserved)
+ // 101-127 custom guids (user-defined guids)
+ // 128-... more custom guids, but requiring multiples bytes for guid table lookup
+
+ // These values aren't currently used, so comment them out
+ // static internal uint KnownGuidIndexLimit = MaximumPossibleKnownGuidIndex;
+ static internal uint MaximumPossibleKnownGuidIndex = 100;
+ static internal uint CustomGuidBaseIndex = MaximumPossibleKnownGuidIndex;
+
+ // This id table includes the Guids that have been added to ISF as ExtendedProperties
+ // Note that they are visible to 3rd party applications
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/InkSerializedFormat/InkSerializer.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/InkSerializedFormat/InkSerializer.cs
new file mode 100644
index 0000000..17f48b5
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/InkSerializedFormat/InkSerializer.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace MS.Internal.Ink.InkSerializedFormat
+{
+ internal class StrokeCollectionSerializer
+ {
+ internal static readonly double AvalonToHimetricMultiplier = 2540.0d / 96.0d;
+ internal static readonly double HimetricToAvalonMultiplier = 96.0d / 2540.0d;
+ }
+}
+
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/Lasso.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/Lasso.cs
new file mode 100644
index 0000000..14ccde6
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/Lasso.cs
@@ -0,0 +1,908 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Windows;
+using System.Collections.Generic;
+using System.Globalization;
+using WpfInk.PresentationCore.System.Windows;
+using WpfInk.PresentationCore.System.Windows.Ink;
+
+namespace MS.Internal.Ink
+{
+ #region Lasso
+
+ ///
+ /// Represents a lasso for selecting/cutting ink strokes with.
+ /// Lasso is a sequence of points defining a complex region (polygon)
+ ///
+ internal class Lasso
+ {
+ #region Constructors
+
+ ///
+ /// Default c-tor. Used in incremental hit-testing.
+ ///
+ internal Lasso()
+ {
+ _points = new List();
+ }
+
+ #endregion
+
+ #region API
+
+ ///
+ /// Returns the bounds of the lasso
+ ///
+ internal Rect Bounds
+ {
+ get { return _bounds; }
+ set { _bounds = value; }
+ }
+
+ ///
+ /// Tells whether the lasso captures any area
+ ///
+ internal bool IsEmpty
+ {
+ get
+ {
+ System.Diagnostics.Debug.Assert(_points != null);
+ // The value is based on the assumption that the lasso is normalized
+ // i.e. it has no duplicate points or collinear sibling segments.
+ return (_points.Count < 3);
+ }
+ }
+
+ ///
+ /// Returns the count of points in the lasso
+ ///
+ internal int PointCount
+ {
+ get
+ {
+ System.Diagnostics.Debug.Assert(_points != null);
+ return _points.Count;
+ }
+ }
+
+ ///
+ /// Index-based read-only accessor to lasso points
+ ///
+ /// index of the point to return
+ /// a point in the lasso
+ internal Point this[int index]
+ {
+ get
+ {
+ System.Diagnostics.Debug.Assert(_points != null);
+ System.Diagnostics.Debug.Assert((0 <= index) && (index < _points.Count));
+
+ return _points[index];
+ }
+ }
+
+ ///
+ /// Extends the lasso by appending more points
+ ///
+ /// new points
+ internal void AddPoints(IEnumerable points)
+ {
+ System.Diagnostics.Debug.Assert(null != points);
+
+ foreach (Point point in points)
+ {
+ AddPoint(point);
+ }
+ }
+
+ ///
+ /// Appends a point to the lasso
+ ///
+ /// new lasso point
+ internal void AddPoint(Point point)
+ {
+ System.Diagnostics.Debug.Assert(_points != null);
+ if (!Filter(point))
+ {
+ // The point is not filtered, add it to the lasso
+ AddPointImpl(point);
+ }
+ }
+
+ ///
+ /// This method implement the core algorithm to check whether a point is within a polygon
+ /// that are formed by the lasso points.
+ ///
+ ///
+ /// true if the point is contained within the lasso; false otherwise
+ internal bool Contains(Point point)
+ {
+ System.Diagnostics.Debug.Assert(_points != null);
+
+ if (false == _bounds.Contains(point))
+ {
+ return false;
+ }
+
+ bool isHigher = false;
+ int last = _points.Count;
+ while (--last >= 0)
+ {
+ if (!DoubleUtil.AreClose(_points[last].Y, point.Y))
+ {
+ isHigher = (point.Y < _points[last].Y);
+ break;
+ }
+ }
+
+ bool isInside = false;
+ Point prevLassoPoint = _points[_points.Count - 1];
+ for (int i = 0; i < _points.Count; i++)
+ {
+ Point lassoPoint = _points[i];
+ if (DoubleUtil.AreClose(lassoPoint.Y, point.Y))
+ {
+ if (DoubleUtil.AreClose(lassoPoint.X, point.X))
+ {
+ isInside = true;
+ break;
+ }
+ if ((0 != i) && DoubleUtil.AreClose(prevLassoPoint.Y, point.Y) &&
+ DoubleUtil.GreaterThanOrClose(point.X, Math.Min(prevLassoPoint.X, lassoPoint.X)) &&
+ DoubleUtil.LessThanOrClose(point.X, Math.Max(prevLassoPoint.X, lassoPoint.X)))
+ {
+ isInside = true;
+ break;
+ }
+ }
+ else if (isHigher != (point.Y < lassoPoint.Y))
+ {
+ isHigher = !isHigher;
+ if (DoubleUtil.GreaterThanOrClose(point.X, Math.Max(prevLassoPoint.X, lassoPoint.X)))
+ {
+ // there certainly is an intersection on the left
+ isInside = !isInside;
+ }
+ else if (DoubleUtil.GreaterThanOrClose(point.X, Math.Min(prevLassoPoint.X, lassoPoint.X)))
+ {
+ // The X of the point lies within the x ranges for the segment.
+ // Calculate the x value of the point where the segment intersects with the line.
+ Vector lassoSegment = lassoPoint - prevLassoPoint;
+ System.Diagnostics.Debug.Assert(lassoSegment.Y != 0);
+ double x = prevLassoPoint.X + (lassoSegment.X / lassoSegment.Y) * (point.Y - prevLassoPoint.Y);
+ if (DoubleUtil.GreaterThanOrClose(point.X, x))
+ {
+ isInside = !isInside;
+ }
+ }
+ }
+ prevLassoPoint = lassoPoint;
+ }
+ return isInside;
+ }
+
+ internal StrokeIntersection[] HitTest(StrokeNodeIterator iterator)
+ {
+ System.Diagnostics.Debug.Assert(_points != null);
+ System.Diagnostics.Debug.Assert(iterator != null);
+
+ if (_points.Count < 3)
+ {
+ //
+ // it takes at least 3 points to create a lasso
+ //
+ return Array.Empty();
+ }
+
+ //
+ // We're about to perform hit testing with a lasso.
+ // To do so we need to iterate through each StrokeNode.
+ // As we do, we calculate the bounding rect between it
+ // and the previous StrokeNode and store this in 'currentStrokeSegmentBounds'
+ //
+ // Next, we check to see if that StrokeNode pair's bounding box intersects
+ // with the bounding box of the Lasso points. If not, we continue iterating through
+ // StrokeNode pairs.
+ //
+ // If it does, we do a more granular hit test by pairing points in the Lasso, getting
+ // their bounding box and seeing if that bounding box intersects our current StrokeNode
+ // pair
+ //
+
+ Point lastNodePosition = new Point();
+ Point lassoLastPoint = _points[_points.Count - 1];
+ Rect currentStrokeSegmentBounds = Rect.Empty;
+
+ // Initilize the current crossing to be an empty one
+ LassoCrossing currentCrossing = LassoCrossing.EmptyCrossing;
+
+ // Creat a list to hold all the crossings
+ List crossingList = new List();
+ for (int i = 0; i < iterator.Count; i++)
+ {
+ StrokeNode strokeNode = iterator[i];
+ Rect nodeBounds = strokeNode.GetBounds();
+ currentStrokeSegmentBounds.Union(nodeBounds);
+
+ // Skip the node if it's outside of the lasso's bounds
+ if (currentStrokeSegmentBounds.IntersectsWith(_bounds) == true)
+ {
+ // currentStrokeSegmentBounds, made up of the bounding box of
+ // this StrokeNode unioned with the last StrokeNode,
+ // intersects the lasso bounding box.
+ //
+ // Now we need to iterate through the lasso points and find out where they cross
+ //
+ Point lastPoint = lassoLastPoint;
+ foreach (Point point in _points)
+ {
+ //
+ // calculate a segment of the lasso from the last point
+ // to the current point
+ //
+ Rect lassoSegmentBounds = new Rect(lastPoint, point);
+
+ //
+ // see if this lasso segment intersects with the current stroke segment
+ //
+ if (!currentStrokeSegmentBounds.IntersectsWith(lassoSegmentBounds))
+ {
+ lastPoint = point;
+ continue;
+ }
+
+ //
+ // the lasso segment DOES intersect with the current stroke segment
+ // find out precisely where
+ //
+ StrokeFIndices strokeFIndices = strokeNode.CutTest(lastPoint, point);
+
+ lastPoint = point;
+ if (strokeFIndices.IsEmpty)
+ {
+ // current lasso segment does not hit the stroke segment, continue with the next lasso point
+ continue;
+ }
+
+ // Create a potentially new crossing for the current hit testing result.
+ LassoCrossing potentialNewCrossing = new LassoCrossing(strokeFIndices, strokeNode);
+
+ // Try to merge with the current crossing. If the merge is succussful (return true), the new crossing is actually
+ // continueing the current crossing, so do not start a new crossing. Otherwise, start a new one and add the existing
+ // one to the list.
+ if (!currentCrossing.Merge(potentialNewCrossing))
+ {
+ // start a new crossing and add the existing on to the list
+ crossingList.Add(currentCrossing);
+ currentCrossing = potentialNewCrossing;
+ }
+ }
+ }
+
+ // Continue with the next node
+ currentStrokeSegmentBounds = nodeBounds;
+ lastNodePosition = strokeNode.Position;
+ }
+
+
+ // Adding the last crossing to the list, if valid
+ if (!currentCrossing.IsEmpty)
+ {
+ crossingList.Add(currentCrossing);
+ }
+
+ // Handle the special case of no intersection at all
+ if (crossingList.Count == 0)
+ {
+ // the stroke was either completely inside the lasso
+ // or outside the lasso
+ if (this.Contains(lastNodePosition))
+ {
+ StrokeIntersection[] strokeIntersections = new StrokeIntersection[1];
+ strokeIntersections[0] = StrokeIntersection.Full;
+ return strokeIntersections;
+ }
+ else
+ {
+ return Array.Empty();
+ }
+ }
+
+ // It is still possible that the current crossing list is not sorted or overlapping.
+ // Sort the list and merge the overlapping ones.
+ SortAndMerge(ref crossingList);
+
+ // Produce the hit test results and store them in a list
+ List strokeIntersectionList = new List();
+ ProduceHitTestResults(crossingList, strokeIntersectionList);
+
+ return strokeIntersectionList.ToArray();
+ }
+
+ ///
+ /// Sort and merge the crossing list
+ ///
+ /// The crossing list to sort/merge
+ private static void SortAndMerge(ref List crossingList)
+ {
+ // Sort the crossings based on the BeginFIndex values
+ crossingList.Sort();
+
+ List mergedList = new List();
+ LassoCrossing mcrossing = LassoCrossing.EmptyCrossing;
+ foreach (LassoCrossing crossing in crossingList)
+ {
+ System.Diagnostics.Debug.Assert(!crossing.IsEmpty && crossing.StartNode.IsValid && crossing.EndNode.IsValid);
+ if (!mcrossing.Merge(crossing))
+ {
+ System.Diagnostics.Debug.Assert(!mcrossing.IsEmpty && mcrossing.StartNode.IsValid && mcrossing.EndNode.IsValid);
+ mergedList.Add(mcrossing);
+ mcrossing = crossing;
+ }
+ }
+ if (!mcrossing.IsEmpty)
+ {
+ System.Diagnostics.Debug.Assert(!mcrossing.IsEmpty && mcrossing.StartNode.IsValid && mcrossing.EndNode.IsValid);
+ mergedList.Add(mcrossing);
+ }
+ crossingList = mergedList;
+ }
+
+
+ ///
+ /// Helper function to find out whether a point is inside the lasso
+ ///
+ private bool SegmentWithinLasso(StrokeNode strokeNode, double fIndex)
+ {
+ bool currentSegmentWithinLasso;
+ if (DoubleUtil.AreClose(fIndex, StrokeFIndices.BeforeFirst))
+ {
+ // This should check against the very first stroke node
+ currentSegmentWithinLasso = this.Contains(strokeNode.GetPointAt(0f));
+ }
+ else if (DoubleUtil.AreClose(fIndex, StrokeFIndices.AfterLast))
+ {
+ // This should check against the last stroke node
+ currentSegmentWithinLasso = this.Contains(strokeNode.Position);
+ }
+ else
+ {
+ currentSegmentWithinLasso = this.Contains(strokeNode.GetPointAt(fIndex));
+ }
+
+ return currentSegmentWithinLasso;
+ }
+
+ ///
+ /// Helper function to find out the hit test result
+ ///
+ private void ProduceHitTestResults(
+ List crossingList, List strokeIntersections)
+ {
+ bool previousSegmentInsideLasso = false;
+ for (int x = 0; x <= crossingList.Count; x++)
+ {
+ bool currentSegmentWithinLasso = false;
+ bool canMerge = true;
+ StrokeIntersection si = new StrokeIntersection();
+ if (x == 0)
+ {
+ si.HitBegin = StrokeFIndices.BeforeFirst;
+ si.InBegin = StrokeFIndices.BeforeFirst;
+ }
+ else
+ {
+ si.InBegin = crossingList[x - 1].FIndices.EndFIndex;
+ si.HitBegin = crossingList[x - 1].FIndices.BeginFIndex;
+ currentSegmentWithinLasso = SegmentWithinLasso(crossingList[x - 1].EndNode, si.InBegin);
+ }
+
+ if (x == crossingList.Count)
+ {
+ // For a special case when the last intersection is something like (1.2, AL).
+ // As a result the last InSegment should be empty.
+ if (DoubleUtil.AreClose(si.InBegin, StrokeFIndices.AfterLast))
+ {
+ si.InEnd = StrokeFIndices.BeforeFirst;
+ }
+ else
+ {
+ si.InEnd = StrokeFIndices.AfterLast;
+ }
+ si.HitEnd = StrokeFIndices.AfterLast;
+ }
+ else
+ {
+ si.InEnd = crossingList[x].FIndices.BeginFIndex;
+
+ // For a speical case when the first intersection is something like (BF, 0.67).
+ // As a result the first InSegment should be empty
+ if (DoubleUtil.AreClose(si.InEnd, StrokeFIndices.BeforeFirst))
+ {
+ System.Diagnostics.Debug.Assert(DoubleUtil.AreClose(si.InBegin, StrokeFIndices.BeforeFirst));
+ si.InBegin = StrokeFIndices.AfterLast;
+ }
+
+ si.HitEnd = crossingList[x].FIndices.EndFIndex;
+ currentSegmentWithinLasso = SegmentWithinLasso(crossingList[x].StartNode, si.InEnd);
+
+ // If both the start and end position of the current crossing is
+ // outside the lasso, the crossing is a hit-only intersection, i.e., the in-segment is empty.
+ if (!currentSegmentWithinLasso && !SegmentWithinLasso(crossingList[x].EndNode, si.HitEnd))
+ {
+ currentSegmentWithinLasso = true;
+ si.HitBegin = crossingList[x].FIndices.BeginFIndex;
+ si.InBegin = StrokeFIndices.AfterLast;
+ si.InEnd = StrokeFIndices.BeforeFirst;
+ canMerge = false;
+ }
+ }
+
+ if (currentSegmentWithinLasso)
+ {
+ if (x > 0 && previousSegmentInsideLasso && canMerge)
+ {
+ // we need to consolidate with the previous segment
+ StrokeIntersection previousIntersection = strokeIntersections[strokeIntersections.Count - 1];
+
+ // For example: previousIntersection = [BF, AL, BF, 0.0027], si = [BF, 0.0027, 0.049, 0.063]
+ if (previousIntersection.InSegment.IsEmpty)
+ {
+ previousIntersection.InBegin = si.InBegin;
+ }
+ previousIntersection.InEnd = si.InEnd;
+ previousIntersection.HitEnd = si.HitEnd;
+ strokeIntersections[strokeIntersections.Count - 1] = previousIntersection;
+ }
+ else
+ {
+ strokeIntersections.Add(si);
+ }
+
+ if (DoubleUtil.AreClose(si.HitEnd, StrokeFIndices.AfterLast))
+ {
+ // The strokeIntersections already cover the end of the stroke. No need to continue.
+ return;
+ }
+ }
+ previousSegmentInsideLasso = currentSegmentWithinLasso;
+ }
+ }
+
+ ///
+ /// This flag is set to true when a lasso point has been modified or removed
+ /// from the list, which will invalidate incremental lasso hitteting
+ ///
+ internal bool IsIncrementalLassoDirty
+ {
+ get
+ {
+ return _incrementalLassoDirty;
+ }
+ set
+ {
+ _incrementalLassoDirty = value;
+ }
+ }
+
+ ///
+ /// Get a reference to the lasso points store
+ ///
+ protected List PointsList
+ {
+ get
+ {
+ return _points;
+ }
+ }
+
+ ///
+ /// Filter out duplicate points (and maybe in the futuer colinear points).
+ /// Return true if the point should be filtered
+ ///
+ protected virtual bool Filter(Point point)
+ {
+ // First point should not be filtered
+ if (0 == _points.Count)
+ {
+ return false;
+ }
+ // ISSUE-2004/06/14-vsmirnov - If the new segment is collinear with the last one,
+ // don't add the point but modify the last point instead.
+ Point lastPoint = _points[_points.Count - 1];
+ Vector vector = point - lastPoint;
+
+ // The point will be filtered out, i.e. not added to the list, if the distance to the previous point is
+ // within the tolerance
+ return (Math.Abs(vector.X) < MinDistance && Math.Abs(vector.Y) < MinDistance);
+ }
+
+ ///
+ /// Implemtnation of add point
+ ///
+ ///
+ protected virtual void AddPointImpl(Point point)
+ {
+ _points.Add(point);
+ _bounds.Union(point);
+ }
+ #endregion
+
+ #region Fields
+
+ private List _points;
+ private Rect _bounds = Rect.Empty;
+ private bool _incrementalLassoDirty = false;
+ private static readonly double MinDistance = 1.0;
+
+ #endregion
+
+ ///
+ /// Simple helper struct used to track where the lasso crosses a stroke
+ /// we should consider making this a class if generics perf is bad for structs
+ ///
+ private struct LassoCrossing : IComparable
+ {
+ internal StrokeFIndices FIndices;
+ internal StrokeNode StartNode;
+ internal StrokeNode EndNode;
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ public LassoCrossing(StrokeFIndices newFIndices, StrokeNode strokeNode)
+ {
+ System.Diagnostics.Debug.Assert(!newFIndices.IsEmpty);
+ System.Diagnostics.Debug.Assert(strokeNode.IsValid);
+ FIndices = newFIndices;
+ StartNode = EndNode = strokeNode;
+ }
+
+ ///
+ /// ToString
+ ///
+ public override string ToString()
+ {
+ return FIndices.ToString();
+ }
+
+ ///
+ /// Construct an empty LassoCrossing
+ ///
+ public static LassoCrossing EmptyCrossing
+ {
+ get
+ {
+ LassoCrossing crossing = new LassoCrossing();
+ crossing.FIndices = StrokeFIndices.Empty;
+ return crossing;
+ }
+ }
+
+ ///
+ /// Return true if this crossing is an empty one; false otherwise
+ ///
+ public bool IsEmpty
+ {
+ get { return FIndices.IsEmpty; }
+ }
+
+ ///
+ /// Implement the interface used for comparison
+ ///
+ ///
+ ///
+ public int CompareTo(object obj)
+ {
+ System.Diagnostics.Debug.Assert(obj is LassoCrossing);
+ LassoCrossing crossing = (LassoCrossing) obj;
+ if (crossing.IsEmpty && this.IsEmpty)
+ {
+ return 0;
+ }
+ else if (crossing.IsEmpty)
+ {
+ return 1;
+ }
+ else if (this.IsEmpty)
+ {
+ return -1;
+ }
+ else
+ {
+ return FIndices.CompareTo(crossing.FIndices);
+ }
+ }
+
+ ///
+ /// Merge two crossings into one.
+ ///
+ ///
+ /// Return true if these two crossings are actually overlapping and merged; false otherwise
+ public bool Merge(LassoCrossing crossing)
+ {
+ if (crossing.IsEmpty)
+ {
+ return false;
+ }
+
+ if (FIndices.IsEmpty && !crossing.IsEmpty)
+ {
+ FIndices = crossing.FIndices;
+ StartNode = crossing.StartNode;
+ EndNode = crossing.EndNode;
+ return true;
+ }
+
+ if (DoubleUtil.GreaterThanOrClose(crossing.FIndices.EndFIndex, FIndices.BeginFIndex) &&
+ DoubleUtil.GreaterThanOrClose(FIndices.EndFIndex, crossing.FIndices.BeginFIndex))
+ {
+ if (DoubleUtil.LessThan(crossing.FIndices.BeginFIndex, FIndices.BeginFIndex))
+ {
+ FIndices.BeginFIndex = crossing.FIndices.BeginFIndex;
+ StartNode = crossing.StartNode;
+ }
+
+ if (DoubleUtil.GreaterThan(crossing.FIndices.EndFIndex, FIndices.EndFIndex))
+ {
+ FIndices.EndFIndex = crossing.FIndices.EndFIndex;
+ EndNode = crossing.EndNode;
+ }
+ return true;
+ }
+
+ return false;
+ }
+ }
+ }
+ #endregion
+
+
+ #region Single-Loop Lasso
+
+ ///
+ /// Implement a special lasso that considers only the first loop
+ ///
+ internal class SingleLoopLasso : Lasso
+ {
+ ///
+ /// Default constructor
+ ///
+ internal SingleLoopLasso() : base() { }
+
+ ///
+ /// Return true if the point will be filtered out and should NOT be added to the list
+ ///
+ protected override bool Filter(Point point)
+ {
+ List points = PointsList;
+
+ // First point should not be filtered
+ if (0 == points.Count)
+ {
+ // Just add the new point to the lasso
+ return false;
+ }
+
+ // Don't add this point if the lasso already has a loop; or
+ // if it's filtered by base class's filter.
+ if (true == _hasLoop || true == base.Filter(point))
+ {
+ // Don't add this point to the lasso.
+ return true;
+ }
+
+ double intersection = 0f;
+
+ // Now check whether the line lastPoint->point intersect with the
+ // existing lasso.
+
+ if (true == GetIntersectionWithExistingLasso(point, ref intersection))
+ {
+ System.Diagnostics.Debug.Assert(intersection >= 0 && intersection <= points.Count - 2);
+
+ if (intersection == points.Count - 2)
+ {
+ return true;
+ }
+
+ // Adding the new point will form a loop
+ int i = (int) intersection;
+
+ if (!DoubleUtil.AreClose(i, intersection))
+ {
+ // Move points[i] to the intersection position
+ Point intersectionPoint = new Point(0, 0);
+ intersectionPoint.X = points[i].X + (intersection - i) * (points[i + 1].X - points[i].X);
+ intersectionPoint.Y = points[i].Y + (intersection - i) * (points[i + 1].Y - points[i].Y);
+ points[i] = intersectionPoint;
+ IsIncrementalLassoDirty = true;
+ }
+
+ // Since the lasso has a self loop and the loop starts at points[i], points[0] to
+ // points[i-1] should be removed
+ if (i > 0)
+ {
+ points.RemoveRange(0, i /*count*/); // Remove points[0] to points[i-1]
+ IsIncrementalLassoDirty = true;
+ }
+
+ if (true == IsIncrementalLassoDirty)
+ {
+ // Update the bounds
+ Rect bounds = Rect.Empty;
+ for (int j = 0; j < points.Count; j++)
+ {
+ bounds.Union(points[j]);
+ }
+ Bounds = bounds;
+ }
+
+ // The lasso has a self_loop, any more points will be neglected.
+ _hasLoop = true;
+
+ // Don't add this point to the lasso.
+ return true;
+ }
+
+ // Just add the new point to the lasso
+ return false;
+ }
+
+ protected override void AddPointImpl(Point point)
+ {
+ _prevBounds = Bounds;
+ base.AddPointImpl(point);
+ }
+
+ ///
+ /// If the line _points[Count -1]->point insersect with the existing lasso, return true
+ /// and bIndex value is set to a doulbe value representing position of the intersection.
+ ///
+ private bool GetIntersectionWithExistingLasso(Point point, ref double bIndex)
+ {
+ List points = PointsList;
+ int count = points.Count;
+
+ Rect newRect = new Rect(points[count - 1], point);
+
+ if (false == _prevBounds.IntersectsWith(newRect))
+ {
+ // The point is not contained in the bound of the existing lasso, no intersection.
+ return false;
+ }
+
+ for (int i = 0; i < count - 2; i++)
+ {
+ Rect currRect = new Rect(points[i], points[i + 1]);
+ if (!currRect.IntersectsWith(newRect))
+ {
+ continue;
+ }
+
+ double s = FindIntersection(points[count - 1] - points[i], /*hitBegin*/
+ point - points[i], /*hitEnd*/
+ new Vector(0, 0), /*orgBegin*/
+ points[i + 1] - points[i] /*orgEnd*/);
+ if (s >= 0 && s <= 1)
+ {
+ // Intersection found, adjust the fIndex
+ bIndex = i + s;
+ return true;
+ }
+ }
+
+ // No intersection
+ return false;
+ }
+
+
+ ///
+ /// Finds the intersection between the segment [hitBegin, hitEnd] and the segment [orgBegin, orgEnd].
+ ///
+ private static double FindIntersection(Vector hitBegin, Vector hitEnd, Vector orgBegin, Vector orgEnd)
+ {
+ System.Diagnostics.Debug.Assert(hitEnd != hitBegin && orgBegin != orgEnd);
+
+ //----------------------------------------------------------------------
+ // Source: http://isc.faqs.org/faqs/graphics/algorithms-faq/
+ // Subject 1.03: How do I find intersections of 2 2D line segments?
+ //
+ // Let A,B,C,D be 2-space position vectors. Then the directed line
+ // segments AB & CD are given by:
+ //
+ // AB=A+r(B-A), r in [0,1]
+ // CD=C+s(D-C), s in [0,1]
+ //
+ // If AB & CD intersect, then
+ //
+ // A+r(B-A)=C+s(D-C), or Ax+r(Bx-Ax)=Cx+s(Dx-Cx)
+ // Ay+r(By-Ay)=Cy+s(Dy-Cy) for some r,s in [0,1]
+ //
+ // Solving the above for r and s yields
+ //
+ // (Ay-Cy)(Dx-Cx)-(Ax-Cx)(Dy-Cy)
+ // r = ----------------------------- (eqn 1)
+ // (Bx-Ax)(Dy-Cy)-(By-Ay)(Dx-Cx)
+ //
+ // (Ay-Cy)(Bx-Ax)-(Ax-Cx)(By-Ay)
+ // s = ----------------------------- (eqn 2)
+ // (Bx-Ax)(Dy-Cy)-(By-Ay)(Dx-Cx)
+ //
+ // Let P be the position vector of the intersection point, then
+ //
+ // P=A+r(B-A) or Px=Ax+r(Bx-Ax) and Py=Ay+r(By-Ay)
+ //
+ // By examining the values of r & s, you can also determine some
+ // other limiting conditions:
+ // If 0 <= r <= 1 && 0 <= s <= 1, intersection exists
+ // r < 0 or r > 1 or s < 0 or s > 1 line segments do not intersect
+ // If the denominator in eqn 1 is zero, AB & CD are parallel
+ // If the numerator in eqn 1 is also zero, AB & CD are collinear.
+ // If they are collinear, then the segments may be projected to the x-
+ // or y-axis, and overlap of the projected intervals checked.
+ //
+ // If the intersection point of the 2 lines are needed (lines in this
+ // context mean infinite lines) regardless whether the two line
+ // segments intersect, then
+ // If r > 1, P is located on extension of AB
+ // If r < 0, P is located on extension of BA
+ // If s > 1, P is located on extension of CD
+ // If s < 0, P is located on extension of DC
+ // Also note that the denominators of eqn 1 & 2 are identical.
+ //
+ // References:
+ // [O'Rourke (C)] pp. 249-51
+ // [Gems III] pp. 199-202 "Faster Line Segment Intersection,"
+ //----------------------------------------------------------------------
+
+ // Calculate the vectors.
+ Vector AB = orgEnd - orgBegin; // B - A
+ Vector CA = orgBegin - hitBegin; // A - C
+ Vector CD = hitEnd - hitBegin; // D - C
+ double det = Vector.Determinant(AB, CD);
+
+ if (DoubleUtil.IsZero(det))
+ {
+ // The segments are parallel. no intersection
+ return NoIntersection;
+ }
+
+ double r = AdjustFIndex(Vector.Determinant(AB, CA) / det);
+
+ if (r >= 0 && r <= 1)
+ {
+ // The line defined AB does cross the segment CD.
+ double s = AdjustFIndex(Vector.Determinant(CD, CA) / det);
+ if (s >= 0 && s <= 1)
+ {
+ // The crossing point is on the segment AB as well.
+ // Intersection found.
+ return s;
+ }
+ }
+
+ // No intersection found
+ return NoIntersection;
+ }
+
+ ///
+ /// Clears double's computation fuzz around 0 and 1
+ ///
+ internal static double AdjustFIndex(double findex)
+ {
+ return DoubleUtil.IsZero(findex) ? 0 : (DoubleUtil.IsOne(findex) ? 1 : findex);
+ }
+
+ private bool _hasLoop = false;
+ private Rect _prevBounds = Rect.Empty;
+ private static readonly double NoIntersection = StrokeFIndices.BeforeFirst;
+ }
+ #endregion
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/Quad.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/Quad.cs
new file mode 100644
index 0000000..4c34b65
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/Quad.cs
@@ -0,0 +1,97 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Collections.Generic;
+using WpfInk.PresentationCore.System.Windows;
+
+namespace MS.Internal.Ink
+{
+ ///
+ /// A helper structure used in StrokeNode and StrokeNodeOperation implementations
+ /// to store endpoints of the quad connecting two nodes of a stroke.
+ /// The vertices of a quad are supposed to be clockwise with points A and D located
+ /// on the begin node and B and C on the end one.
+ ///
+ internal struct Quad
+ {
+ #region Statics
+
+ private static readonly Quad s_empty = new Quad(new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0));
+
+ #endregion
+
+ #region API
+
+ /// Returns the static object representing an empty (unitialized) quad
+ internal static Quad Empty { get { return s_empty; } }
+
+ /// Constructor
+ internal Quad(Point a, Point b, Point c, Point d)
+ {
+ _A = a; _B = b; _C = c; _D = d;
+ }
+
+ /// The A vertex of the quad
+ internal Point A { get { return _A; } set { _A = value; } }
+
+ /// The B vertex of the quad
+ internal Point B { get { return _B; } set { _B = value; } }
+
+ /// The C vertex of the quad
+ internal Point C { get { return _C; } set { _C = value; } }
+
+ /// The D vertex of the quad
+ internal Point D { get { return _D; } set { _D = value; } }
+
+ // Returns quad's vertex by index where A is of the index 0, B - is 1, etc
+ internal Point this[int index]
+ {
+ get
+ {
+ switch (index)
+ {
+ case 0: return _A;
+ case 1: return _B;
+ case 2: return _C;
+ case 3: return _D;
+ default:
+ throw new IndexOutOfRangeException("index");
+ }
+ }
+ }
+
+ /// Tells whether the quad is invalid (empty)
+ internal bool IsEmpty
+ {
+ get { return (_A == _B) && (_C == _D); }
+ }
+
+ internal void GetPoints(List pointBuffer)
+ {
+ pointBuffer.Add(_A);
+ pointBuffer.Add(_B);
+ pointBuffer.Add(_C);
+ pointBuffer.Add(_D);
+ }
+
+ /// Returns the bounds of the quad
+ internal Rect Bounds
+ {
+ get { return IsEmpty ? Rect.Empty : Rect.Union(new Rect(_A, _B), new Rect(_C, _D)); }
+ }
+
+ #endregion
+
+ #region Fields
+
+ private Point _A;
+ private Point _B;
+ private Point _C;
+ private Point _D;
+
+ #endregion
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeFIndices.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeFIndices.cs
new file mode 100644
index 0000000..3e30530
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeFIndices.cs
@@ -0,0 +1,208 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Globalization;
+
+namespace MS.Internal.Ink
+{
+ #region StrokeFIndices
+
+ ///
+ /// A helper struct that represents a fragment of a stroke spine.
+ ///
+ internal struct StrokeFIndices : IEquatable
+ {
+ #region Private statics
+ private static readonly StrokeFIndices s_empty = new StrokeFIndices(AfterLast, BeforeFirst);
+ private static readonly StrokeFIndices s_full = new StrokeFIndices(BeforeFirst, AfterLast);
+ #endregion
+
+ #region Internal API
+
+ ///
+ /// BeforeFirst
+ ///
+ ///
+ internal static double BeforeFirst { get { return double.MinValue; } }
+
+ ///
+ /// AfterLast
+ ///
+ ///
+ internal static double AfterLast { get { return double.MaxValue; } }
+
+ ///
+ /// StrokeFIndices
+ ///
+ /// beginFIndex
+ /// endFIndex
+ internal StrokeFIndices(double beginFIndex, double endFIndex)
+ {
+ _beginFIndex = beginFIndex;
+ _endFIndex = endFIndex;
+ }
+
+ ///
+ /// BeginFIndex
+ ///
+ ///
+ internal double BeginFIndex
+ {
+ get { return _beginFIndex; }
+ set { _beginFIndex = value; }
+ }
+
+ ///
+ /// EndFIndex
+ ///
+ ///
+ internal double EndFIndex
+ {
+ get { return _endFIndex; }
+ set { _endFIndex = value; }
+ }
+
+ ///
+ /// ToString
+ ///
+ public override string ToString()
+ {
+ return "{" + GetStringRepresentation(_beginFIndex) + "," + GetStringRepresentation(_endFIndex) + "}";
+ }
+
+ ///
+ /// Equals
+ ///
+ ///
+ ///
+ public bool Equals(StrokeFIndices strokeFIndices)
+ {
+ return (strokeFIndices == this);
+ }
+
+ ///
+ /// Equals
+ ///
+ ///
+ ///
+ public override bool Equals(Object obj)
+ {
+ // Check for null and compare run-time types
+ if (obj == null || GetType() != obj.GetType())
+ return false;
+ return ((StrokeFIndices) obj == this);
+ }
+
+ ///
+ /// GetHashCode
+ ///
+ ///
+ public override int GetHashCode()
+ {
+ return _beginFIndex.GetHashCode() ^ _endFIndex.GetHashCode();
+ }
+
+ ///
+ /// operator ==
+ ///
+ ///
+ ///
+ ///
+ public static bool operator ==(StrokeFIndices sfiLeft, StrokeFIndices sfiRight)
+ {
+ return (DoubleUtil.AreClose(sfiLeft._beginFIndex, sfiRight._beginFIndex)
+ && DoubleUtil.AreClose(sfiLeft._endFIndex, sfiRight._endFIndex));
+ }
+
+ ///
+ /// operator !=
+ ///
+ ///
+ ///
+ ///
+ public static bool operator !=(StrokeFIndices sfiLeft, StrokeFIndices sfiRight)
+ {
+ return !(sfiLeft == sfiRight);
+ }
+
+ internal static string GetStringRepresentation(double fIndex)
+ {
+ if (DoubleUtil.AreClose(fIndex, StrokeFIndices.BeforeFirst))
+ {
+ return "BeforeFirst";
+ }
+ if (DoubleUtil.AreClose(fIndex, StrokeFIndices.AfterLast))
+ {
+ return "AfterLast";
+ }
+ return fIndex.ToString(CultureInfo.InvariantCulture);
+ }
+
+ ///
+ ///
+ ///
+ internal static StrokeFIndices Empty { get { return s_empty; } }
+
+ ///
+ ///
+ ///
+ internal static StrokeFIndices Full { get { return s_full; } }
+
+ ///
+ ///
+ ///
+ internal bool IsEmpty { get { return DoubleUtil.GreaterThanOrClose(_beginFIndex, _endFIndex); } }
+
+ ///
+ ///
+ ///
+ internal bool IsFull { get { return ((DoubleUtil.AreClose(_beginFIndex, BeforeFirst)) && (DoubleUtil.AreClose(_endFIndex, AfterLast))); } }
+
+
+#if DEBUG
+ ///
+ ///
+ ///
+ private bool IsValid { get { return !double.IsNaN(_beginFIndex) && !double.IsNaN(_endFIndex) && _beginFIndex < _endFIndex; } }
+
+#endif
+
+ ///
+ /// Compare StrokeFIndices based on the BeinFIndex
+ ///
+ ///
+ ///
+ internal int CompareTo(StrokeFIndices fIndices)
+ {
+#if DEBUG
+ System.Diagnostics.Debug.Assert(!double.IsNaN(_beginFIndex) && !double.IsNaN(_endFIndex) && DoubleUtil.LessThan(_beginFIndex, _endFIndex));
+#endif
+ if (DoubleUtil.AreClose(BeginFIndex, fIndices.BeginFIndex))
+ {
+ return 0;
+ }
+ else if (DoubleUtil.GreaterThan(BeginFIndex, fIndices.BeginFIndex))
+ {
+ return 1;
+ }
+ else
+ {
+ return -1;
+ }
+ }
+
+ #endregion
+
+ #region Fields
+
+ private double _beginFIndex;
+ private double _endFIndex;
+
+ #endregion
+ }
+
+ #endregion
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeIntersection.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeIntersection.cs
new file mode 100644
index 0000000..0648f1f
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeIntersection.cs
@@ -0,0 +1,255 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Collections.Generic;
+using MS.Internal.Ink;
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ ///
+ /// A helper struct that represents a fragment of a stroke spine.
+ ///
+ internal struct StrokeIntersection
+ {
+ #region Private statics
+ private static StrokeIntersection s_empty = new StrokeIntersection(AfterLast, AfterLast, BeforeFirst, BeforeFirst);
+ private static StrokeIntersection s_full = new StrokeIntersection(BeforeFirst, BeforeFirst, AfterLast, AfterLast);
+ #endregion
+
+ #region Public API
+
+ ///
+ /// BeforeFirst
+ ///
+ ///
+ internal static double BeforeFirst { get { return StrokeFIndices.BeforeFirst; } }
+
+ ///
+ /// AfterLast
+ ///
+ ///
+ internal static double AfterLast { get { return StrokeFIndices.AfterLast; } }
+
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal StrokeIntersection(double hitBegin, double inBegin, double inEnd, double hitEnd)
+ {
+ //ISSUE-2004/12/06-XiaoTu: should we validate the input?
+ _hitSegment = new StrokeFIndices(hitBegin, hitEnd);
+ _inSegment = new StrokeFIndices(inBegin, inEnd);
+ }
+
+ ///
+ /// hitBeginFIndex
+ ///
+ ///
+ internal double HitBegin
+ {
+ set { _hitSegment.BeginFIndex = value; }
+ }
+
+ ///
+ /// hitEndFIndex
+ ///
+ ///
+ internal double HitEnd
+ {
+ get { return _hitSegment.EndFIndex; }
+ set { _hitSegment.EndFIndex = value; }
+ }
+
+
+ ///
+ /// InBegin
+ ///
+ ///
+ internal double InBegin
+ {
+ get { return _inSegment.BeginFIndex; }
+ set { _inSegment.BeginFIndex = value; }
+ }
+
+ ///
+ /// InEnd
+ ///
+ ///
+ internal double InEnd
+ {
+ get { return _inSegment.EndFIndex; }
+ set { _inSegment.EndFIndex = value; }
+ }
+
+ ///
+ /// ToString
+ ///
+ public override string ToString()
+ {
+ return "{" + StrokeFIndices.GetStringRepresentation(_hitSegment.BeginFIndex) + ","
+ + StrokeFIndices.GetStringRepresentation(_inSegment.BeginFIndex) + ","
+ + StrokeFIndices.GetStringRepresentation(_inSegment.EndFIndex) + ","
+ + StrokeFIndices.GetStringRepresentation(_hitSegment.EndFIndex) + "}";
+ }
+
+
+ ///
+ /// Equals
+ ///
+ ///
+ ///
+ public override bool Equals(Object obj)
+ {
+ // Check for null and compare run-time types
+ if (obj == null || GetType() != obj.GetType())
+ return false;
+ return ((StrokeIntersection) obj == this);
+ }
+
+ ///
+ /// GetHashCode
+ ///
+ ///
+ public override int GetHashCode()
+ {
+ return _hitSegment.GetHashCode() ^ _inSegment.GetHashCode();
+ }
+
+
+ ///
+ /// operator ==
+ ///
+ ///
+ ///
+ ///
+ public static bool operator ==(StrokeIntersection left, StrokeIntersection right)
+ {
+ return (left._hitSegment == right._hitSegment && left._inSegment == right._inSegment);
+ }
+
+ ///
+ /// operator !=
+ ///
+ ///
+ ///
+ ///
+ public static bool operator !=(StrokeIntersection left, StrokeIntersection right)
+ {
+ return !(left == right);
+ }
+
+ #endregion
+
+ #region Internal API
+
+ ///
+ ///
+ ///
+ internal static StrokeIntersection Full { get { return s_full; } }
+
+ ///
+ ///
+ ///
+ internal bool IsEmpty { get { return _hitSegment.IsEmpty; } }
+
+
+ ///
+ ///
+ ///
+ internal StrokeFIndices HitSegment
+ {
+ get { return _hitSegment; }
+ }
+
+ ///
+ ///
+ ///
+ internal StrokeFIndices InSegment
+ {
+ get { return _inSegment; }
+ }
+
+ #endregion
+
+ #region Internal static methods
+
+ ///
+ /// Get the "in-segments" of the intersections.
+ ///
+ internal static StrokeFIndices[] GetInSegments(StrokeIntersection[] intersections)
+ {
+ global::System.Diagnostics.Debug.Assert(intersections != null);
+ global::System.Diagnostics.Debug.Assert(intersections.Length > 0);
+
+ List inFIndices = new List(intersections.Length);
+ for (int j = 0; j < intersections.Length; j++)
+ {
+ global::System.Diagnostics.Debug.Assert(!intersections[j].IsEmpty);
+ if (!intersections[j].InSegment.IsEmpty)
+ {
+ if (inFIndices.Count > 0 &&
+ inFIndices[inFIndices.Count - 1].EndFIndex >=
+ intersections[j].InSegment.BeginFIndex)
+ {
+ //merge
+ StrokeFIndices sfiPrevious = inFIndices[inFIndices.Count - 1];
+ sfiPrevious.EndFIndex = intersections[j].InSegment.EndFIndex;
+ inFIndices[inFIndices.Count - 1] = sfiPrevious;
+ }
+ else
+ {
+ inFIndices.Add(intersections[j].InSegment);
+ }
+ }
+ }
+ return inFIndices.ToArray();
+ }
+
+ ///
+ /// Get the "hit-segments"
+ ///
+ internal static StrokeFIndices[] GetHitSegments(StrokeIntersection[] intersections)
+ {
+ global::System.Diagnostics.Debug.Assert(intersections != null);
+ global::System.Diagnostics.Debug.Assert(intersections.Length > 0);
+
+ List hitFIndices = new List(intersections.Length);
+ for (int j = 0; j < intersections.Length; j++)
+ {
+ global::System.Diagnostics.Debug.Assert(!intersections[j].IsEmpty);
+ if (!intersections[j].HitSegment.IsEmpty)
+ {
+ if (hitFIndices.Count > 0 &&
+ hitFIndices[hitFIndices.Count - 1].EndFIndex >=
+ intersections[j].HitSegment.BeginFIndex)
+ {
+ //merge
+ StrokeFIndices sfiPrevious = hitFIndices[hitFIndices.Count - 1];
+ sfiPrevious.EndFIndex = intersections[j].HitSegment.EndFIndex;
+ hitFIndices[hitFIndices.Count - 1] = sfiPrevious;
+ }
+ else
+ {
+ hitFIndices.Add(intersections[j].HitSegment);
+ }
+ }
+ }
+ return hitFIndices.ToArray();
+ }
+
+ #endregion
+
+ #region Fields
+
+ private StrokeFIndices _hitSegment;
+ private StrokeFIndices _inSegment;
+
+ #endregion
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNode.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNode.cs
new file mode 100644
index 0000000..ae0a44a
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNode.cs
@@ -0,0 +1,1106 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+//#define DEBUG_RENDERING_FEEDBACK
+
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Input;
+using System.Diagnostics;
+using WpfInk.PresentationCore.System.Windows;
+
+namespace MS.Internal.Ink
+{
+ #region StrokeNode
+
+ ///
+ /// StrokeNode represents a single segment on a stroke spine.
+ /// It's used in enumerating through basic geometries making a stroke contour.
+ ///
+ internal struct StrokeNode
+ {
+ #region Constructors
+
+ ///
+ /// Constructor.
+ ///
+ /// StrokeNodeOperations object created for particular rendering
+ /// Index of the node on the stroke spine
+ /// StrokeNodeData for this node
+ /// StrokeNodeData for the precedeng node
+ /// Whether the current node is the last node
+ internal StrokeNode(
+ StrokeNodeOperations operations,
+ int index,
+ in StrokeNodeData nodeData,
+ in StrokeNodeData lastNodeData,
+ bool isLastNode)
+ {
+ System.Diagnostics.Debug.Assert(operations != null);
+ System.Diagnostics.Debug.Assert((nodeData.IsEmpty == false) && (index >= 0));
+
+
+ _operations = operations;
+ _index = index;
+ _thisNode = nodeData;
+ _lastNode = lastNodeData;
+ _isQuadCached = lastNodeData.IsEmpty;
+ _connectingQuad = Quad.Empty;
+ _isLastNode = isLastNode;
+ }
+
+ #endregion
+
+ #region Public API
+
+ ///
+ /// Position of the node on the stroke spine.
+ ///
+ ///
+ internal Point Position { get { return _thisNode.Position; } }
+
+ ///
+ /// Position of the previous StrokeNode
+ ///
+ ///
+ internal Point PreviousPosition { get { return _lastNode.Position; } }
+
+ ///
+ /// PressureFactor of the node on the stroke spine.
+ ///
+ ///
+ internal float PressureFactor { get { return _thisNode.PressureFactor; } }
+
+ ///
+ /// PressureFactor of the previous StrokeNode
+ ///
+ ///
+ internal float PreviousPressureFactor { get { return _lastNode.PressureFactor; } }
+
+ ///
+ /// Tells whether the node shape (the stylus shape used in the rendering)
+ /// is elliptical or polygonal. If the shape is an ellipse, GetContourPoints
+ /// returns the control points for the quadratic Bezier that defines the ellipse.
+ ///
+ /// true if the shape is ellipse, false otherwise
+ internal bool IsEllipse { get { return IsValid && _operations.IsNodeShapeEllipse; } }
+
+ ///
+ /// Returns true if this is the last node in the enumerator
+ ///
+ internal bool IsLastNode { get { return _isLastNode; } }
+
+ ///
+ /// Returns the bounds of the node shape w/o connecting quadrangle
+ ///
+ ///
+ internal Rect GetBounds()
+ {
+ return IsValid ? _operations.GetNodeBounds(_thisNode) : Rect.Empty;
+ }
+
+ ///
+ /// Returns the bounds of the node shape and connecting quadrangle
+ ///
+ ///
+ internal Rect GetBoundsConnected()
+ {
+ return IsValid ? Rect.Union(_operations.GetNodeBounds(_thisNode), ConnectingQuad.Bounds) : Rect.Empty;
+ }
+
+ ///
+ /// Returns the points that make up the stroke node shape (minus the connecting quad)
+ ///
+ internal void GetContourPoints(List pointBuffer)
+ {
+ if (IsValid)
+ {
+ _operations.GetNodeContourPoints(_thisNode, pointBuffer);
+ }
+ }
+
+ ///
+ /// Returns the points that make up the stroke node shape (minus the connecting quad)
+ ///
+ internal void GetPreviousContourPoints(List pointBuffer)
+ {
+ if (IsValid)
+ {
+ _operations.GetNodeContourPoints(_lastNode, pointBuffer);
+ }
+ }
+
+ ///
+ /// Returns the connecting quad
+ ///
+ internal Quad GetConnectingQuad()
+ {
+ if (IsValid)
+ {
+ return ConnectingQuad;
+ }
+ return Quad.Empty;
+ }
+
+ /////
+ ///// IsPointWithinRectOrEllipse
+ /////
+ //internal bool IsPointWithinRectOrEllipse(Point point, double xRadiusOrHalfWidth, double yRadiusOrHalfHeight, Point center, bool isEllipse)
+ //{
+ // if (isEllipse)
+ // {
+ // //determine what delta is required to move the rect to be
+ // //centered at 0,0
+ // double xDelta = center.X + xRadiusOrHalfWidth;
+ // double yDelta = center.Y + yRadiusOrHalfHeight;
+
+ // //offset the point by that delta
+ // point.X -= xDelta;
+ // point.Y -= yDelta;
+
+ // //formula for ellipse is x^2/a^2 + y^2/b^2 = 1
+ // double a = xRadiusOrHalfWidth;
+ // double b = yRadiusOrHalfHeight;
+ // double res = (((point.X * point.X) / (a * a)) +
+ // ((point.Y * point.Y) / (b * b)));
+
+ // if (res <= 1)
+ // {
+ // return true;
+ // }
+ // return false;
+ // }
+ // else
+ // {
+ // if (point.X >= (center.X - xRadiusOrHalfWidth) &&
+ // point.X <= (center.X + xRadiusOrHalfWidth) &&
+ // point.Y >= (center.Y - yRadiusOrHalfHeight) &&
+ // point.Y <= (center.Y + yRadiusOrHalfHeight))
+ // {
+ // return true;
+ // }
+ // return false;
+ // }
+ //}
+
+ ///
+ /// GetPointsAtStartOfSegment
+ ///
+ internal void GetPointsAtStartOfSegment(List abPoints,
+ List dcPoints
+#if DEBUG_RENDERING_FEEDBACK
+ , DrawingContext debugDC, double feedbackSize, bool showFeedback
+#endif
+ )
+ {
+ if (IsValid)
+ {
+ Quad quad = ConnectingQuad;
+ if (IsEllipse)
+ {
+ Rect startNodeBounds = _operations.GetNodeBounds(_lastNode);
+
+ //add instructions to arc from D to A
+ abPoints.Add(quad.D);
+ abPoints.Add(StrokeRenderer.ArcToMarker);
+ abPoints.Add(new Point(startNodeBounds.Width, startNodeBounds.Height));
+ abPoints.Add(quad.A);
+
+ //simply start at D
+ dcPoints.Add(quad.D);
+
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(null, new Pen(Brushes.Pink, feedbackSize / 2), _lastNode.Position, startNodeBounds.Width / 2, startNodeBounds.Height / 2);
+ debugDC.DrawEllipse(Brushes.Red, null, quad.A, feedbackSize, feedbackSize);
+ debugDC.DrawEllipse(Brushes.Blue, null, quad.D, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ else
+ {
+ //we're interested in the A, D points as well as the
+ //nodecountour points between them
+ Rect endNodeRect = _operations.GetNodeBounds(_thisNode);
+
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawRectangle(null, new Pen(Brushes.Pink, feedbackSize / 2), _operations.GetNodeBounds(_lastNode));
+ }
+#endif
+ Vector[] vertices = _operations.GetVertices();
+ double pressureFactor = _lastNode.PressureFactor;
+ int maxCount = vertices.Length * 2;
+ int i = 0;
+ bool dIsInEndNode = true;
+ for (; i < maxCount; i++)
+ {
+ //look for the d point first
+ Point point = _lastNode.Position + (vertices[i % vertices.Length] * pressureFactor);
+ if (point == quad.D)
+ {
+ //ab always starts with the D position (only add if it's not in endNode's bounds)
+ if (!endNodeRect.Contains(quad.D))
+ {
+ dIsInEndNode = false;
+ abPoints.Add(quad.D);
+ dcPoints.Add(quad.D);
+ }
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Blue, null, quad.D, feedbackSize, feedbackSize);
+ }
+#endif
+ break;
+ }
+ }
+
+ if (i == maxCount)
+ {
+ Debug.Assert(false, "StrokeNodeOperations.GetPointsAtStartOfSegment failed to find the D position");
+ //we didn't find the d point, return
+ return;
+ }
+
+
+ //now look for the A position
+ //advance i
+ i++;
+ for (int j = 0; i < maxCount && j < vertices.Length; i++, j++)
+ {
+ //look for the A point now
+ Point point = _lastNode.Position + (vertices[i % vertices.Length] * pressureFactor);
+ //add everything in between to ab as long as it's not already in endNode's bounds
+ if (!endNodeRect.Contains(point))
+ {
+ abPoints.Add(point);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Wheat, null, point, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ if (dIsInEndNode)
+ {
+ Debug.Assert(!endNodeRect.Contains(point));
+
+ //add the first point after d, clockwise
+ dIsInEndNode = false;
+ dcPoints.Add(point);
+ }
+ if (point == quad.A)
+ {
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Red, null, point, feedbackSize, feedbackSize);
+ }
+#endif
+ break;
+ }
+ }
+ }
+ }
+ }
+
+
+ ///
+ /// GetPointsAtEndOfSegment
+ ///
+ internal void GetPointsAtEndOfSegment(List abPoints,
+ List dcPoints
+#if DEBUG_RENDERING_FEEDBACK
+ , DrawingContext debugDC, double feedbackSize, bool showFeedback
+#endif
+ )
+ {
+ if (IsValid)
+ {
+ Quad quad = ConnectingQuad;
+ if (IsEllipse)
+ {
+ Rect bounds = GetBounds();
+ //add instructions to arc from D to A
+ abPoints.Add(quad.B);
+ abPoints.Add(StrokeRenderer.ArcToMarker);
+ abPoints.Add(new Point(bounds.Width, bounds.Height));
+ abPoints.Add(quad.C);
+
+ //don't add to the dc points
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(null, new Pen(Brushes.Pink, feedbackSize / 2), _thisNode.Position, bounds.Width / 2, bounds.Height / 2);
+ debugDC.DrawEllipse(Brushes.Green, null, quad.B, feedbackSize, feedbackSize);
+ debugDC.DrawEllipse(Brushes.Yellow, null, quad.C, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ else
+ {
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawRectangle(null, new Pen(Brushes.Pink, feedbackSize / 2), GetBounds());
+ }
+#endif
+ //we're interested in the B, C points as well as the
+ //nodecountour points between them
+ double pressureFactor = _thisNode.PressureFactor;
+ Vector[] vertices = _operations.GetVertices();
+ int maxCount = vertices.Length * 2;
+ int i = 0;
+ for (; i < maxCount; i++)
+ {
+ //look for the d point first
+ Point point = _thisNode.Position + (vertices[i % vertices.Length] * pressureFactor);
+ if (point == quad.B)
+ {
+ abPoints.Add(quad.B);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Green, null, point, feedbackSize, feedbackSize);
+ }
+#endif
+ break;
+ }
+ }
+
+ if (i == maxCount)
+ {
+ Debug.Assert(false, "StrokeNodeOperations.GetPointsAtEndOfSegment failed to find the B position");
+ //we didn't find the d point, return
+ return;
+ }
+
+ //now look for the C position
+ //advance i
+ i++;
+ for (int j = 0; i < maxCount && j < vertices.Length; i++, j++)
+ {
+ //look for the c point last
+ Point point = _thisNode.Position + (vertices[i % vertices.Length] * pressureFactor);
+ if (point == quad.C)
+ {
+ break;
+ }
+ //only add to ab if we didn't find C
+ abPoints.Add(point);
+
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Wheat, null, quad.C, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ //finally, add the D point
+ dcPoints.Add(quad.C);
+
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Yellow, null, quad.C, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ }
+ }
+
+ ///
+ /// GetPointsAtMiddleSegment
+ ///
+ internal void GetPointsAtMiddleSegment(StrokeNode previous,
+ double angleBetweenNodes,
+ List abPoints,
+ List dcPoints,
+ out bool missingIntersection
+#if DEBUG_RENDERING_FEEDBACK
+ , DrawingContext debugDC, double feedbackSize, bool showFeedback
+#endif
+ )
+ {
+ missingIntersection = false;
+ if (IsValid && previous.IsValid)
+ {
+ Quad quad1 = previous.ConnectingQuad;
+ if (!quad1.IsEmpty)
+ {
+ Quad quad2 = ConnectingQuad;
+ if (!quad2.IsEmpty)
+ {
+ if (IsEllipse)
+ {
+ Rect node1Bounds = _operations.GetNodeBounds(previous._lastNode);
+ Rect node2Bounds = _operations.GetNodeBounds(_lastNode);
+ Rect node3Bounds = _operations.GetNodeBounds(_thisNode);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(null, new Pen(Brushes.Pink, feedbackSize / 2), _lastNode.Position, node2Bounds.Width / 2, node2Bounds.Height / 2);
+ }
+#endif
+ if (angleBetweenNodes == 0.0d || ((quad1.B == quad2.A) && (quad1.C == quad2.D)))
+ {
+ //quads connections are the same, just add them
+ abPoints.Add(quad1.B);
+ dcPoints.Add(quad1.C);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Green, null, quad1.B, feedbackSize, feedbackSize);
+ debugDC.DrawEllipse(Brushes.Yellow, null, quad1.C, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ else if (angleBetweenNodes > 0.0)
+ {
+ //the stroke angled towards the AB side
+ //this part is easy
+ if (quad1.B == quad2.A)
+ {
+ abPoints.Add(quad1.B);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Green, null, quad1.B, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ else
+ {
+ Point intersection = GetIntersection(quad1.A, quad1.B, quad2.A, quad2.B);
+ Rect union = Rect.Union(node1Bounds, node2Bounds);
+ union.Inflate(1.0, 1.0);
+ //make sure we're not off in space
+
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Green, null, quad1.B, feedbackSize * 1.5, feedbackSize * 1.5);
+ debugDC.DrawEllipse(Brushes.Red, null, quad2.A, feedbackSize, feedbackSize);
+ }
+#endif
+
+ if (union.Contains(intersection))
+ {
+ abPoints.Add(intersection);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Orange, null, intersection, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ else
+ {
+ //if we missed the intersection we'll need to close the stroke segment
+ //this work is done in StrokeRenderer
+ missingIntersection = true;
+ return; //we're done.
+ }
+ }
+
+ if (quad1.C == quad2.D)
+ {
+ dcPoints.Add(quad1.C);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Yellow, null, quad1.C, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ else
+ {
+ //add instructions to arc from quad1.C to quad2.D in reverse order (since we walk this array backwards to render)
+ dcPoints.Add(quad1.C);
+ dcPoints.Add(new Point(node2Bounds.Width, node2Bounds.Height));
+ dcPoints.Add(StrokeRenderer.ArcToMarker);
+ dcPoints.Add(quad2.D);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Yellow, null, quad1.C, feedbackSize, feedbackSize);
+ debugDC.DrawEllipse(Brushes.Blue, null, quad2.D, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ }
+ else
+ {
+ //the stroke angled towards the CD side
+ //this part is easy
+ if (quad1.C == quad2.D)
+ {
+ dcPoints.Add(quad1.C);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Yellow, null, quad1.C, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ else
+ {
+ Point intersection = GetIntersection(quad1.D, quad1.C, quad2.D, quad2.C);
+ Rect union = Rect.Union(node1Bounds, node2Bounds);
+ union.Inflate(1.0, 1.0);
+ //make sure we're not off in space
+
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Yellow, null, quad1.C, feedbackSize * 1.5, feedbackSize * 1.5);
+ debugDC.DrawEllipse(Brushes.Blue, null, quad2.D, feedbackSize, feedbackSize);
+ }
+#endif
+
+ if (union.Contains(intersection))
+ {
+ dcPoints.Add(intersection);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Orange, null, intersection, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ else
+ {
+ //if we missed the intersection we'll need to close the stroke segment
+ //this work is done in StrokeRenderer
+ missingIntersection = true;
+ return; //we're done.
+ }
+ }
+
+ if (quad1.B == quad2.A)
+ {
+ abPoints.Add(quad1.B);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Green, null, quad1.B, feedbackSize, feedbackSize);
+ }
+#endif
+
+ }
+ else
+ {
+ //we need to arc between quad1.B and quad2.A along node2
+ abPoints.Add(quad1.B);
+ abPoints.Add(StrokeRenderer.ArcToMarker);
+ abPoints.Add(new Point(node2Bounds.Width, node2Bounds.Height));
+ abPoints.Add(quad2.A);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Green, null, quad1.B, feedbackSize, feedbackSize);
+ debugDC.DrawEllipse(Brushes.Red, null, quad2.A, feedbackSize, feedbackSize);
+ }
+#endif
+ }
+ }
+ }
+ else
+ {
+ //rectangle
+ int indexA = -1;
+ int indexB = -1;
+ int indexC = -1;
+ int indexD = -1;
+
+ Vector[] vertices = _operations.GetVertices();
+ double pressureFactor = _lastNode.PressureFactor;
+ for (int i = 0; i < vertices.Length; i++)
+ {
+ Point point = _lastNode.Position + (vertices[i % vertices.Length] * pressureFactor);
+ if (point == quad2.A)
+ {
+ indexA = i;
+ }
+ if (point == quad1.B)
+ {
+ indexB = i;
+ }
+ if (point == quad1.C)
+ {
+ indexC = i;
+ }
+ if (point == quad2.D)
+ {
+ indexD = i;
+ }
+ }
+
+ if (indexA == -1 || indexB == -1 || indexC == -1 || indexD == -1)
+ {
+ Debug.Assert(false, "Couldn't find all 4 indexes in StrokeNodeOperations.GetPointsAtMiddleSegment");
+ return;
+ }
+
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+
+ debugDC.DrawRectangle(null, new Pen(Brushes.Pink, feedbackSize / 2), _operations.GetNodeBounds(_lastNode));
+ debugDC.DrawEllipse(Brushes.Red, null, quad2.A, feedbackSize, feedbackSize);
+ debugDC.DrawEllipse(Brushes.Green, null, quad1.B, feedbackSize, feedbackSize);
+ debugDC.DrawEllipse(Brushes.Yellow, null, quad1.C, feedbackSize, feedbackSize);
+ debugDC.DrawEllipse(Brushes.Blue, null, quad2.D, feedbackSize, feedbackSize);
+ }
+#endif
+
+ Rect node3Rect = _operations.GetNodeBounds(_thisNode);
+ //take care of a-b first
+ if (indexA == indexB)
+ {
+ //quad connection is the same, just add it
+ if (!node3Rect.Contains(quad1.B))
+ {
+ abPoints.Add(quad1.B);
+ }
+ }
+ else if ((indexA == 0 && indexB == 3) || ((indexA != 3 || indexB != 0) && (indexA > indexB)))
+ {
+ if (!node3Rect.Contains(quad1.B))
+ {
+ abPoints.Add(quad1.B);
+ }
+ if (!node3Rect.Contains(quad2.A))
+ {
+ abPoints.Add(quad2.A);
+ }
+ }
+ else
+ {
+ Point intersection = GetIntersection(quad1.A, quad1.B, quad2.A, quad2.B);
+ Rect node12 = Rect.Union(_operations.GetNodeBounds(previous._lastNode), _operations.GetNodeBounds(_lastNode));
+ node12.Inflate(1.0, 1.0);
+ //make sure we're not off in space
+ if (node12.Contains(intersection))
+ {
+ abPoints.Add(intersection);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Orange, null, intersection, feedbackSize, feedbackSize * 1.5);
+ }
+#endif
+ }
+ else
+ {
+ //if we missed the intersection we'll need to close the stroke segment
+ //this work is done in StrokeRenderer.
+ missingIntersection = true;
+ return; //we're done.
+ }
+ }
+
+ // now take care of c-d.
+ if (indexC == indexD)
+ {
+ //quad connection is the same, just add it
+ if (!node3Rect.Contains(quad1.C))
+ {
+ dcPoints.Add(quad1.C);
+ }
+ }
+ else if ((indexC == 0 && indexD == 3) || ((indexC != 3 || indexD != 0) && (indexC > indexD)))
+ {
+ if (!node3Rect.Contains(quad1.C))
+ {
+ dcPoints.Add(quad1.C);
+ }
+ if (!node3Rect.Contains(quad2.D))
+ {
+ dcPoints.Add(quad2.D);
+ }
+ }
+ else
+ {
+ Point intersection = GetIntersection(quad1.D, quad1.C, quad2.D, quad2.C);
+ Rect node12 = Rect.Union(_operations.GetNodeBounds(previous._lastNode), _operations.GetNodeBounds(_lastNode));
+ node12.Inflate(1.0, 1.0);
+ //make sure we're not off in space
+ if (node12.Contains(intersection))
+ {
+ dcPoints.Add(intersection);
+#if DEBUG_RENDERING_FEEDBACK
+ if (showFeedback)
+ {
+ debugDC.DrawEllipse(Brushes.Orange, null, intersection, feedbackSize, feedbackSize * 1.5);
+ }
+#endif
+ }
+ else
+ {
+ //if we missed the intersection we'll need to close the stroke segment
+ //this work is done in StrokeRenderer.
+ missingIntersection = true;
+ return; //we're done.
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Returns the intersection between two lines. This code assumes there is an intersection
+ /// and should only be called if that assumption is valid
+ ///
+ ///
+ internal static Point GetIntersection(Point line1Start, Point line1End, Point line2Start, Point line2End)
+ {
+ double a1 = line1End.Y - line1Start.Y;
+ double b1 = line1Start.X - line1End.X;
+ double c1 = (line1End.X * line1Start.Y) - (line1Start.X * line1End.Y);
+ double a2 = line2End.Y - line2Start.Y;
+ double b2 = line2Start.X - line2End.X;
+ double c2 = (line2End.X * line2Start.Y) - (line2Start.X * line2End.Y);
+
+ double d = (a1 * b2) - (a2 * b1);
+ if (d != 0.0)
+ {
+ double x = ((b1 * c2) - (b2 * c1)) / d;
+ double y = ((a2 * c1) - (a1 * c2)) / d;
+
+ //capture the min and max points
+ double line1XMin, line1XMax, line1YMin, line1YMax, line2XMin, line2XMax, line2YMin, line2YMax;
+ if (line1Start.X < line1End.X)
+ {
+ line1XMin = Math.Floor(line1Start.X);
+ line1XMax = Math.Ceiling(line1End.X);
+ }
+ else
+ {
+ line1XMin = Math.Floor(line1End.X);
+ line1XMax = Math.Ceiling(line1Start.X);
+ }
+
+ if (line2Start.X < line2End.X)
+ {
+ line2XMin = Math.Floor(line2Start.X);
+ line2XMax = Math.Ceiling(line2End.X);
+ }
+ else
+ {
+ line2XMin = Math.Floor(line2End.X);
+ line2XMax = Math.Ceiling(line2Start.X);
+ }
+
+ if (line1Start.Y < line1End.Y)
+ {
+ line1YMin = Math.Floor(line1Start.Y);
+ line1YMax = Math.Ceiling(line1End.Y);
+ }
+ else
+ {
+ line1YMin = Math.Floor(line1End.Y);
+ line1YMax = Math.Ceiling(line1Start.Y);
+ }
+
+ if (line2Start.Y < line2End.Y)
+ {
+ line2YMin = Math.Floor(line2Start.Y);
+ line2YMax = Math.Ceiling(line2End.Y);
+ }
+ else
+ {
+ line2YMin = Math.Floor(line2End.Y);
+ line2YMax = Math.Ceiling(line2Start.Y);
+ }
+
+
+ // now see if we have an intersection between the lines
+ // and not just the projection of the lines
+ if ((line1XMin <= x && x <= line1XMax) &&
+ (line1YMin <= y && y <= line1YMax) &&
+ (line2XMin <= x && x <= line2XMax) &&
+ (line2YMin <= y && y <= line2YMax))
+ {
+ return new Point(x, y);
+ }
+ }
+
+ if ((long) line1End.X == (long) line2Start.X &&
+ (long) line1End.Y == (long) line2Start.Y)
+ {
+ return new Point(line1End.X, line1End.Y);
+ }
+
+ return new Point(Double.NaN, Double.NaN);
+ }
+
+ ///
+ /// This method tells whether the contour of a given stroke node
+ /// intersects with the contour of this node. The contours of both nodes
+ /// include their connecting quadrangles.
+ ///
+ ///
+ ///
+ internal bool HitTest(StrokeNode hitNode)
+ {
+ if (!IsValid || !hitNode.IsValid)
+ {
+ return false;
+ }
+
+ IEnumerable hittingContour = hitNode.GetContourSegments();
+
+ return _operations.HitTest(_lastNode, _thisNode, ConnectingQuad, hittingContour);
+ }
+
+ ///
+ /// Finds out if a given node intersects with this one,
+ /// and returns findices of the intersection.
+ ///
+ ///
+ ///
+ internal StrokeFIndices CutTest(StrokeNode hitNode)
+ {
+ if ((IsValid == false) || (hitNode.IsValid == false))
+ {
+ return StrokeFIndices.Empty;
+ }
+
+ IEnumerable hittingContour = hitNode.GetContourSegments();
+
+ // If the node contours intersect, the result is a pair of findices
+ // this segment should be cut at to let the hitNode's contour through it.
+ StrokeFIndices cutAt = _operations.CutTest(_lastNode, _thisNode, ConnectingQuad, hittingContour);
+
+ return (_index == 0) ? cutAt : BindFIndices(cutAt);
+ }
+
+ ///
+ /// Finds out if a given linear segment intersects with the contour of this node
+ /// (including connecting quadrangle), and returns findices of the intersection.
+ ///
+ ///
+ ///
+ ///
+ internal StrokeFIndices CutTest(Point begin, Point end)
+ {
+ if (IsValid == false)
+ {
+ return StrokeFIndices.Empty;
+ }
+
+ // If the node contours intersect, the result is a pair of findices
+ // this segment should be cut at to let the hitNode's contour through it.
+ StrokeFIndices cutAt = _operations.CutTest(_lastNode, _thisNode, ConnectingQuad, begin, end);
+
+ System.Diagnostics.Debug.Assert(!double.IsNaN(cutAt.BeginFIndex) && !double.IsNaN(cutAt.EndFIndex));
+
+ // Bind the found findices to the node and return the result
+ return BindFIndicesForLassoHitTest(cutAt);
+ }
+
+ #endregion
+
+ #region Private helpers
+
+ ///
+ /// Binds a local fragment to this node by setting the integer part of the
+ /// fragment findices equal to the index of the previous node
+ ///
+ ///
+ ///
+ private StrokeFIndices BindFIndices(StrokeFIndices fragment)
+ {
+ System.Diagnostics.Debug.Assert(IsValid && (_index >= 0));
+
+ if (fragment.IsEmpty == false)
+ {
+ // Adjust only findices which are on this segment of thew spine (i.e. between 0 and 1)
+ if (!DoubleUtil.AreClose(fragment.BeginFIndex, StrokeFIndices.BeforeFirst))
+ {
+ System.Diagnostics.Debug.Assert(fragment.BeginFIndex >= 0 && fragment.BeginFIndex <= 1);
+ fragment.BeginFIndex += _index - 1;
+ }
+ if (!DoubleUtil.AreClose(fragment.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ System.Diagnostics.Debug.Assert(fragment.EndFIndex >= 0 && fragment.EndFIndex <= 1);
+ fragment.EndFIndex += _index - 1;
+ }
+ }
+ return fragment;
+ }
+
+ internal int Index
+ {
+ get { return _index; }
+ }
+
+ ///
+ /// Bind the StrokeFIndices for lasso hit test results.
+ ///
+ ///
+ ///
+ private StrokeFIndices BindFIndicesForLassoHitTest(StrokeFIndices fragment)
+ {
+ System.Diagnostics.Debug.Assert(IsValid);
+ if (!fragment.IsEmpty)
+ {
+ // Adjust BeginFIndex
+ if (DoubleUtil.AreClose(fragment.BeginFIndex, StrokeFIndices.BeforeFirst))
+ {
+ // set it to be the index of the previous node, indicating intersection start from previous node
+ fragment.BeginFIndex = (_index == 0 ? StrokeFIndices.BeforeFirst : _index - 1);
+ }
+ else
+ {
+ // Adjust findices which are on this segment of the spine (i.e. between 0 and 1)
+ System.Diagnostics.Debug.Assert(DoubleUtil.GreaterThanOrClose(fragment.BeginFIndex, 0f));
+
+ System.Diagnostics.Debug.Assert(DoubleUtil.LessThanOrClose(fragment.BeginFIndex, 1f));
+
+ // Adjust the value to consider index, say from 0.75 to 3.75 (for _index = 4)
+ fragment.BeginFIndex += _index - 1;
+ }
+
+ //Adjust EndFIndex
+ if (DoubleUtil.AreClose(fragment.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ // set it to be the index of the current node, indicating the intersection cover the end of the node
+ fragment.EndFIndex = (_isLastNode ? StrokeFIndices.AfterLast : _index);
+ }
+ else
+ {
+ System.Diagnostics.Debug.Assert(DoubleUtil.GreaterThanOrClose(fragment.EndFIndex, 0f));
+
+ System.Diagnostics.Debug.Assert(DoubleUtil.LessThanOrClose(fragment.EndFIndex, 1f));
+ // Ajust the value to consider the index
+ fragment.EndFIndex += _index - 1;
+ }
+ }
+ return fragment;
+ }
+
+ ///
+ /// Tells whether the StrokeNode instance is valid or not (created via the default ctor)
+ ///
+ internal bool IsValid { get { return _operations != null; } }
+
+ ///
+ /// The quadrangle that connects this and the previous node.
+ /// Can be empty if this node is the first one or if one of the nodes is
+ /// completely inside the other.
+ /// The type Quad is supposed to be internal even if we surface StrokeNode.
+ /// External users of StrokeNode should use GetConnectionPoints instead.
+ ///
+ private Quad ConnectingQuad
+ {
+ get
+ {
+ System.Diagnostics.Debug.Assert(IsValid);
+
+ if (_isQuadCached == false)
+ {
+ _connectingQuad = _operations.GetConnectingQuad(_lastNode, _thisNode);
+ _isQuadCached = true;
+ }
+ return _connectingQuad;
+ }
+ }
+
+ ///
+ /// Returns an enumerator for edges of the contour comprised by the node
+ /// and connecting quadrangle (_lastNode is excluded)
+ /// Used for hit-testing a stroke against an other stroke (stroke and point erasing)
+ ///
+ private IEnumerable GetContourSegments()
+ {
+ System.Diagnostics.Debug.Assert(IsValid);
+
+ // Calls thru to the StrokeNodeOperations object
+ if (IsEllipse)
+ {
+ // ISSUE-2004/06/15- temporary workaround to avoid hit-testing with ellipses
+ return _operations.GetNonBezierContourSegments(_lastNode, _thisNode);
+ }
+ return _operations.GetContourSegments(_thisNode, ConnectingQuad);
+ }
+
+ ///
+ /// Returns the spine point that corresponds to the given findex.
+ ///
+ /// A local findex between the previous index and this one (ex: between 2.0 and 3.0)
+ /// Point on the spine
+ internal Point GetPointAt(double findex)
+ {
+ System.Diagnostics.Debug.Assert(IsValid);
+
+ if (_lastNode.IsEmpty)
+ {
+ System.Diagnostics.Debug.Assert(findex == 0);
+ return _thisNode.Position;
+ }
+
+ System.Diagnostics.Debug.Assert((findex >= _index - 1) && (findex <= _index));
+
+ if (DoubleUtil.AreClose(findex, (double) _index))
+ {
+ //
+ // we're being asked for this exact point
+ // if we don't return it here, our algorithm
+ // below doesn't work
+ //
+ return _thisNode.Position;
+ }
+
+ //
+ // get the spare change to the left of the decimal point
+ // eg turn 2.75 into .75
+ //
+ double floor = Math.Floor(findex);
+ findex = findex - floor;
+
+ double xDiff = (_thisNode.Position.X - _lastNode.Position.X) * findex;
+ double yDiff = (_thisNode.Position.Y - _lastNode.Position.Y) * findex;
+
+ //
+ // return the previous point plus the delta's
+ //
+ return new Point(_lastNode.Position.X + xDiff,
+ _lastNode.Position.Y + yDiff);
+ }
+
+ #endregion
+
+ #region Fields
+
+ // Internal objects created for particular rendering
+ private StrokeNodeOperations _operations;
+
+ // Node's index on the stroke spine
+ private int _index;
+
+ // This and the previous node data that used by the StrokeNodeOperations object to build
+ // and/or hit-test the contour of the node/segment
+ private StrokeNodeData _thisNode;
+ private StrokeNodeData _lastNode;
+
+ // Calculating of the connecting quadrangle is not a cheap operations, therefore,
+ // first, it's computed only by request, and second, once computed it's cached in the StrokeNode
+ private bool _isQuadCached;
+ private Quad _connectingQuad;
+
+ // Is the current stroke node the last node?
+ private bool _isLastNode;
+
+ #endregion
+ }
+ #endregion
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeData.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeData.cs
new file mode 100644
index 0000000..7fdd508
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeData.cs
@@ -0,0 +1,86 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Input;
+using System.Diagnostics;
+using WpfInk.PresentationCore.System.Windows;
+
+namespace MS.Internal.Ink
+{
+ #region StrokeNodeData
+
+ ///
+ /// This structure represents a node on a stroke spine.
+ ///
+ internal readonly struct StrokeNodeData
+ {
+ #region Statics
+
+ private static readonly StrokeNodeData s_empty = new StrokeNodeData();
+
+ #endregion
+
+ #region API (internal)
+
+ /// Returns static object representing an unitialized node
+ internal static StrokeNodeData Empty { get { return s_empty; } }
+
+ ///
+ /// Constructor for nodes of a pressure insensitive stroke
+ ///
+ /// position of the node
+ internal StrokeNodeData(Point position)
+ {
+ _position = position;
+ _pressure = 1;
+ }
+
+ ///
+ /// Constructor for nodes with pressure data
+ ///
+ /// position of the node
+ /// pressure scaling factor at the node
+ internal StrokeNodeData(Point position, float pressure)
+ {
+ System.Diagnostics.Debug.Assert(DoubleUtil.GreaterThan((double) pressure, 0d));
+
+ _position = position;
+ _pressure = pressure;
+ }
+
+ /// Tells whether the structre was properly initialized
+ internal bool IsEmpty
+ {
+ get
+ {
+ Debug.Assert(DoubleUtil.AreClose(0, s_empty._pressure));
+ return DoubleUtil.AreClose(_pressure, s_empty._pressure);
+ }
+ }
+
+ /// Position of the node
+ internal Point Position
+ {
+ get { return _position; }
+ }
+
+ /// Pressure scaling factor at the node
+ internal float PressureFactor { get { return _pressure; } }
+
+ #endregion
+
+ #region Privates
+
+ private readonly Point _position;
+ private readonly float _pressure;
+
+ #endregion
+ }
+
+ #endregion
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeEnumerator.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeEnumerator.cs
new file mode 100644
index 0000000..9fcfb67
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeEnumerator.cs
@@ -0,0 +1,248 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using WpfInk.PresentationCore.System.Windows;
+using WpfInk.PresentationCore.System.Windows.Ink;
+using WpfInk.PresentationCore.System.Windows.Input.Stylus;
+
+namespace MS.Internal.Ink
+{
+ ///
+ /// This class serves as a unified tool for enumerating through stroke nodes
+ /// for all kinds of rendering and/or hit-testing that uses stroke contours.
+ /// It provides static API for static (atomic) rendering, and it needs to be
+ /// instantiated for dynamic (incremental) rendering. It generates stroke nodes
+ /// from Stroke objects with or w/o overriden drawing attributes, as well as from
+ /// a arrays of points (for a given StylusShape), and from raw stylus packets.
+ /// In either case, the output collection of nodes is represented by a disposable
+ /// iterator (i.e. good for a single enumeration only).
+ ///
+ internal class StrokeNodeIterator
+ {
+ ///
+ /// Helper wrapper
+ ///
+ public static StrokeNodeIterator GetIterator(Stroke stroke, DrawingAttributes drawingAttributes)
+ {
+ if (stroke == null)
+ {
+ throw new System.ArgumentNullException("stroke");
+ }
+ if (drawingAttributes == null)
+ {
+ throw new System.ArgumentNullException("drawingAttributes");
+ }
+
+ StylusPointCollection stylusPoints =
+ drawingAttributes.FitToCurve ? stroke.GetBezierStylusPoints() : stroke.StylusPoints;
+
+ return GetIterator(stylusPoints, drawingAttributes);
+ }
+ ///
+ /// Creates a default enumerator for a given stroke
+ /// If using the strokes drawing attributes, pass stroke.DrawingAttributes for the second
+ /// argument. If using an overridden DA, use that instance.
+ ///
+ internal static StrokeNodeIterator GetIterator(StylusPointCollection stylusPoints, DrawingAttributes drawingAttributes)
+ {
+ if (stylusPoints == null)
+ {
+ throw new System.ArgumentNullException("stylusPoints");
+ }
+ if (drawingAttributes == null)
+ {
+ throw new System.ArgumentNullException("drawingAttributes");
+ }
+
+ StrokeNodeOperations operations =
+ StrokeNodeOperations.CreateInstance(drawingAttributes.StylusShape);
+
+ bool usePressure = !drawingAttributes.IgnorePressure;
+
+ return new StrokeNodeIterator(stylusPoints, operations, usePressure);
+ }
+
+
+ ///
+ /// GetNormalizedPressureFactor
+ ///
+ private static float GetNormalizedPressureFactor(float stylusPointPressureFactor)
+ {
+ //
+ // create a compatible pressure value that maps 0-1 to 0.25 - 1.75
+ //
+ return (1.5f * stylusPointPressureFactor) + 0.25f;
+ }
+
+ ///
+ /// Constructor for an incremental node enumerator that builds nodes
+ /// from array(s) of points and a given stylus shape.
+ ///
+ /// a shape that defines the stroke contour
+ internal StrokeNodeIterator(StylusShape nodeShape)
+ : this(null, //stylusPoints
+ StrokeNodeOperations.CreateInstance(nodeShape),
+ false) //usePressure)
+ {
+ }
+
+ ///
+ /// Constructor for an incremental node enumerator that builds nodes
+ /// from StylusPointCollections
+ /// called by the IncrementalRenderer
+ ///
+ /// drawing attributes
+ internal StrokeNodeIterator(DrawingAttributes drawingAttributes)
+ : this(null, //stylusPoints
+ StrokeNodeOperations.CreateInstance((drawingAttributes == null ? null : drawingAttributes.StylusShape)),
+ (drawingAttributes == null ? false : !drawingAttributes.IgnorePressure)) //usePressure
+ {
+ }
+
+ ///
+ /// Private ctor
+ ///
+ ///
+ ///
+ ///
+ internal StrokeNodeIterator(StylusPointCollection stylusPoints,
+ StrokeNodeOperations operations,
+ bool usePressure)
+ {
+ //Note, StylusPointCollection can be null
+ _stylusPoints = stylusPoints;
+ if (operations == null)
+ {
+ throw new ArgumentNullException("operations");
+ }
+ _operations = operations;
+ _usePressure = usePressure;
+ }
+
+ ///
+ /// Generates (enumerates) StrokeNode objects for a stroke increment
+ /// represented by an StylusPointCollection. Called from IncrementalRenderer
+ ///
+ /// StylusPointCollection
+ /// yields StrokeNode objects one by one
+ internal StrokeNodeIterator GetIteratorForNextSegment(StylusPointCollection stylusPoints)
+ {
+ if (stylusPoints == null)
+ {
+ throw new System.ArgumentNullException("stylusPoints");
+ }
+
+ if (_stylusPoints != null && _stylusPoints.Count > 0 && stylusPoints.Count > 0)
+ {
+ //insert the previous last point, but we need insert a compatible
+ //previous point. The easiest way to do this is to clone a point
+ //(since StylusPoint is a struct, we get get one out to get a copy
+ StylusPoint sp = stylusPoints[0];
+ StylusPoint lastStylusPoint = _stylusPoints[_stylusPoints.Count - 1];
+ sp.X = lastStylusPoint.X;
+ sp.Y = lastStylusPoint.Y;
+ sp.PressureFactor = lastStylusPoint.PressureFactor;
+ stylusPoints.Insert(0, sp);
+ }
+
+ return new StrokeNodeIterator(stylusPoints,
+ _operations,
+ _usePressure);
+ }
+
+ ///
+ /// Generates (enumerates) StrokeNode objects for a stroke increment
+ /// represented by an array of points. This method is supposed to be used only
+ /// on objects created via the c-tor with a StylusShape parameter.
+ ///
+ /// an array of points representing a stroke increment
+ /// yields StrokeNode objects one by one
+ internal StrokeNodeIterator GetIteratorForNextSegment(Point[] points)
+ {
+ if (points == null)
+ {
+ throw new System.ArgumentNullException("points");
+ }
+ StylusPointCollection newStylusPoints = new StylusPointCollection(points);
+ if (_stylusPoints != null && _stylusPoints.Count > 0)
+ {
+ //insert the previous last point
+ newStylusPoints.Insert(0, _stylusPoints[_stylusPoints.Count - 1]);
+ }
+
+ return new StrokeNodeIterator(newStylusPoints,
+ _operations,
+ _usePressure);
+ }
+
+ ///
+ /// The count of strokenodes that can be iterated across
+ ///
+ internal int Count
+ {
+ get
+ {
+ if (_stylusPoints == null)
+ {
+ return 0;
+ }
+ return _stylusPoints.Count;
+ }
+ }
+
+ ///
+ /// Gets a StrokeNode at the specified index
+ ///
+ ///
+ ///
+ internal StrokeNode this[int index]
+ {
+ get
+ {
+ return this[index, (index == 0 ? -1 : index - 1)];
+ }
+ }
+
+ ///
+ /// Gets a StrokeNode at the specified index that connects to a stroke at the previousIndex
+ /// previousIndex can be -1 to signify it should be empty (first strokeNode)
+ ///
+ ///
+ internal StrokeNode this[int index, int previousIndex]
+ {
+ get
+ {
+ if (_stylusPoints == null || index < 0 || index >= _stylusPoints.Count || previousIndex < -1 || previousIndex >= index)
+ {
+ throw new IndexOutOfRangeException();
+ }
+
+ StylusPoint stylusPoint = _stylusPoints[index];
+ StylusPoint previousStylusPoint = (previousIndex == -1 ? new StylusPoint() : _stylusPoints[previousIndex]);
+ float pressureFactor = 1.0f;
+ float previousPressureFactor = 1.0f;
+ if (_usePressure)
+ {
+ pressureFactor = StrokeNodeIterator.GetNormalizedPressureFactor(stylusPoint.PressureFactor);
+ previousPressureFactor = StrokeNodeIterator.GetNormalizedPressureFactor(previousStylusPoint.PressureFactor);
+ }
+
+ StrokeNodeData nodeData = new StrokeNodeData((Point) stylusPoint, pressureFactor);
+ StrokeNodeData lastNodeData = StrokeNodeData.Empty;
+ if (previousIndex != -1)
+ {
+ lastNodeData = new StrokeNodeData((Point) previousStylusPoint, previousPressureFactor);
+ }
+
+ //we use previousIndex+1 because index can skip ahead
+ return new StrokeNode(_operations, previousIndex + 1, nodeData, lastNodeData, index == _stylusPoints.Count - 1 /*Is this the last node?*/);
+ }
+ }
+
+ private bool _usePressure;
+ private StrokeNodeOperations _operations;
+ private StylusPointCollection _stylusPoints;
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeOperations.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeOperations.cs
new file mode 100644
index 0000000..0865b5e
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeOperations.cs
@@ -0,0 +1,1331 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Collections.Generic;
+using WpfInk.PresentationCore.System.Windows;
+using WpfInk.PresentationCore.System.Windows.Ink;
+
+namespace MS.Internal.Ink
+{
+ ///
+ /// The base operations class that implements polygonal node operations by default.
+ ///
+ internal partial class StrokeNodeOperations
+ {
+ #region Static API
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static StrokeNodeOperations CreateInstance(StylusShape nodeShape)
+ {
+ if (nodeShape == null)
+ {
+ throw new ArgumentNullException("nodeShape");
+ }
+ if (nodeShape.IsEllipse)
+ {
+ return new EllipticalNodeOperations(nodeShape);
+ }
+ return new StrokeNodeOperations(nodeShape);
+ }
+ #endregion
+
+ #region API
+
+ ///
+ /// Constructor
+ ///
+ /// shape of the nodes
+ internal StrokeNodeOperations(StylusShape nodeShape)
+ {
+ System.Diagnostics.Debug.Assert(nodeShape != null);
+ _vertices = nodeShape.GetVerticesAsVectors();
+ }
+
+ ///
+ /// This is probably not the best (design-wise) but the cheapest way to tell
+ /// EllipticalNodeOperations from all other implementations of node operations.
+ ///
+ internal virtual bool IsNodeShapeEllipse { get { return false; } }
+
+ ///
+ /// Computes the bounds of a node
+ ///
+ /// node to compute bounds of
+ /// bounds of the node
+ internal Rect GetNodeBounds(in StrokeNodeData node)
+ {
+ if (_shapeBounds.IsEmpty)
+ {
+ int i;
+ for (i = 0; (i + 1) < _vertices.Length; i += 2)
+ {
+ _shapeBounds.Union(new Rect((Point) _vertices[i], (Point) _vertices[i + 1]));
+ }
+ if (i < _vertices.Length)
+ {
+ _shapeBounds.Union((Point) _vertices[i]);
+ }
+ }
+
+ Rect boundingBox = _shapeBounds;
+ System.Diagnostics.Debug.Assert((boundingBox.X <= 0) && (boundingBox.Y <= 0));
+
+ double pressureFactor = node.PressureFactor;
+ if (!DoubleUtil.AreClose(pressureFactor, 1d))
+ {
+ boundingBox = new Rect(
+ _shapeBounds.X * pressureFactor,
+ _shapeBounds.Y * pressureFactor,
+ _shapeBounds.Width * pressureFactor,
+ _shapeBounds.Height * pressureFactor);
+ }
+
+ boundingBox.Location += (Vector) node.Position;
+
+ return boundingBox;
+ }
+
+ internal void GetNodeContourPoints(in StrokeNodeData node, List pointBuffer)
+ {
+ double pressureFactor = node.PressureFactor;
+ if (DoubleUtil.AreClose(pressureFactor, 1d))
+ {
+ for (int i = 0; i < _vertices.Length; i++)
+ {
+ pointBuffer.Add(node.Position + _vertices[i]);
+ }
+ }
+ else
+ {
+ for (int i = 0; i < _vertices.Length; i++)
+ {
+ pointBuffer.Add(node.Position + (_vertices[i] * pressureFactor));
+ }
+ }
+ }
+
+ ///
+ /// Returns an enumerator for edges of the contour comprised by a given node
+ /// and its connecting quadrangle.
+ /// Used for hit-testing a stroke against an other stroke (stroke and point erasing)
+ ///
+ /// node
+ /// quadrangle connecting the node to the preceeding node
+ /// contour segments enumerator
+ internal virtual IEnumerable GetContourSegments(StrokeNodeData node, Quad quad)
+ {
+ System.Diagnostics.Debug.Assert(node.IsEmpty == false);
+
+ if (quad.IsEmpty)
+ {
+ Point vertex = node.Position + (_vertices[_vertices.Length - 1] * node.PressureFactor);
+ for (int i = 0; i < _vertices.Length; i++)
+ {
+ Point nextVertex = node.Position + (_vertices[i] * node.PressureFactor);
+ yield return new ContourSegment(vertex, nextVertex);
+ vertex = nextVertex;
+ }
+ }
+ else
+ {
+ yield return new ContourSegment(quad.A, quad.B);
+
+ for (int i = 0, count = _vertices.Length; i < count; i++)
+ {
+ Point vertex = node.Position + (_vertices[i] * node.PressureFactor);
+ if (vertex == quad.B)
+ {
+ for (int j = 0; (j < count) && (vertex != quad.C); j++)
+ {
+ i = (i + 1) % count;
+ Point nextVertex = node.Position + (_vertices[i] * node.PressureFactor);
+ yield return new ContourSegment(vertex, nextVertex);
+ vertex = nextVertex;
+ }
+ break;
+ }
+ }
+
+ yield return new ContourSegment(quad.C, quad.D);
+ yield return new ContourSegment(quad.D, quad.A);
+ }
+ }
+
+ ///
+ /// ISSUE-2004/06/15- temporary workaround to avoid hit-testing ellipses with ellipses
+ ///
+ ///
+ ///
+ ///
+ internal virtual IEnumerable GetNonBezierContourSegments(StrokeNodeData beginNode, StrokeNodeData endNode)
+ {
+ Quad quad = beginNode.IsEmpty ? Quad.Empty : GetConnectingQuad(beginNode, endNode);
+ return GetContourSegments(endNode, quad);
+ }
+
+
+ ///
+ /// Finds connecting points for a pair of stroke nodes (of a polygonal shape)
+ ///
+ /// a node to connect
+ /// another node, next to beginNode
+ /// connecting quadrangle, that can be empty if one node is inside the other
+ internal virtual Quad GetConnectingQuad(in StrokeNodeData beginNode, in StrokeNodeData endNode)
+ {
+ // Return an empty quad if either of the nodes is empty (not a node)
+ // or if both nodes are at the same position.
+ if (beginNode.IsEmpty || endNode.IsEmpty || DoubleUtil.AreClose(beginNode.Position, endNode.Position))
+ {
+ return Quad.Empty;
+ }
+
+ // By definition, Quad's vertices (A,B,C,D) are ordered clockwise with points A and D located
+ // on the beginNode and B and C on the endNode. Basically, we're looking for segments AB and CD.
+ // We iterate through the vertices of the beginNode, at each vertex we analyze location of the
+ // connecting segment relative to the node's edges at the vertex, and enforce these rules:
+ // - if the vector of the connecting segment at a vertex V[i] is on the left from vector V[i]V[i+1]
+ // and not on the left from vector V[i-1]V[i], then it's the AB segment of the quad (V[i] == A).
+ // - if the vector of the connecting segment at a vertex V[i] is on the left from vector V[i-1]V[i]
+ // and not on the left from vector V[i]V[i+1], then it's the CD segment of the quad (V[i] == D).
+ //
+
+ Quad quad = Quad.Empty;
+ bool foundAB = false, foundCD = false;
+
+ // There's no need to build shapes of the two nodes in order to find their connecting quad.
+ // It's the spine vector between the nodes and their scaling diff (pressure delta) is all
+ // that matters here.
+ Vector spine = endNode.Position - beginNode.Position;
+ double pressureDelta = endNode.PressureFactor - beginNode.PressureFactor;
+
+ // Iterate through the vertices of the default shape
+ int count = _vertices.Length;
+ for (int i = 0, j = count - 1; i < count; i++, j = ((j + 1) % count))
+ {
+ // Compute vector of the connecting segment at the vertex [i]
+ Vector connection = spine + _vertices[i] * pressureDelta;
+ if ((pressureDelta != 0) && (connection.X == 0) && (connection.Y == 0))
+ {
+ // One of the nodes, |----|
+ // as well as the connecting quad, |__ |
+ // is entirely inside the other node. | | |
+ // [i] --> |__|_|
+ return Quad.Empty;
+ }
+
+ // Find out where this vector is about the node edge [i][i+1]
+ // (The vars names "goingTo" and "comingFrom" refer direction of the line defined
+ // by the connecting vector applied at vertex [i], relative to the contour of the node shape.
+ // Using these terms, (comingFrom != Right && goingTo == Left) corresponds to the segment AB,
+ // and (comingFrom == Right && goingTo != Left) describes the DC.
+ HitResult goingTo = WhereIsVectorAboutVector(connection, _vertices[(i + 1) % count] - _vertices[i]);
+
+ if (goingTo == HitResult.Left)
+ {
+ if (false == foundAB)
+ {
+ // Find out where the node edge [i-1][i] is about the connecting vector
+ HitResult comingFrom = WhereIsVectorAboutVector(_vertices[i] - _vertices[j], connection);
+ if (HitResult.Right != comingFrom)
+ {
+ foundAB = true;
+ quad.A = beginNode.Position + _vertices[i] * beginNode.PressureFactor;
+ quad.B = endNode.Position + _vertices[i] * endNode.PressureFactor;
+ if (true == foundCD)
+ {
+ // Found all 4 points. Break out from the 'for' loop.
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ if (false == foundCD)
+ {
+ // Find out where the node edge [i-1][i] is about the connecting vector
+ HitResult comingFrom = WhereIsVectorAboutVector(_vertices[i] - _vertices[j], connection);
+ if (HitResult.Right == comingFrom)
+ {
+ foundCD = true;
+ quad.C = endNode.Position + _vertices[i] * endNode.PressureFactor;
+ quad.D = beginNode.Position + _vertices[i] * beginNode.PressureFactor;
+ if (true == foundAB)
+ {
+ // Found all 4 points. Break out from the 'for' loop.
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (!foundAB || !foundCD || // (2)
+ ((pressureDelta != 0) && Vector.Determinant(quad.B - quad.A, quad.D - quad.A) == 0)) // (1)
+ {
+ // _____ _______
+ // One of the nodes, (1) |__ | (2) | ___ |
+ // as well as the connecting quad, | | | | | | |
+ // is entirely inside the other node. |__| | | |__| |
+ // |____| |___ __|
+ return Quad.Empty;
+ }
+
+ return quad;
+ }
+
+ ///
+ /// Hit-tests ink segment defined by two nodes against a linear segment.
+ ///
+ /// Begin node of the ink segment
+ /// End node of the ink segment
+ /// Pre-computed quadrangle connecting the two ink nodes
+ /// Begin point of the hitting segment
+ /// End point of the hitting segment
+ /// true if there's intersection, false otherwise
+ internal virtual bool HitTest(
+ in StrokeNodeData beginNode, in StrokeNodeData endNode, Quad quad, Point hitBeginPoint, Point hitEndPoint)
+ {
+ // Check for special cases when the endNode is the very first one (beginNode.IsEmpty)
+ // or one node is completely inside the other. In either case the connecting quad
+ // would be Empty and we need to hit-test against the biggest node (the one with
+ // the greater PressureFactor)
+ if (quad.IsEmpty)
+ {
+ Point position;
+ double pressureFactor;
+ if (beginNode.IsEmpty || (endNode.PressureFactor > beginNode.PressureFactor))
+ {
+ position = endNode.Position;
+ pressureFactor = endNode.PressureFactor;
+ }
+ else
+ {
+ position = beginNode.Position;
+ pressureFactor = beginNode.PressureFactor;
+ }
+
+ // Find the coordinates of the hitting segment relative to the ink node
+ Vector hitBegin = hitBeginPoint - position, hitEnd = hitEndPoint - position;
+ if (pressureFactor != 1)
+ {
+ // Instead of applying pressure to the node, do reverse scaling on
+ // the hitting segment. This allows us use the original array of vertices
+ // in hit-testing.
+ System.Diagnostics.Debug.Assert(DoubleUtil.IsZero(pressureFactor) == false);
+ hitBegin /= pressureFactor;
+ hitEnd /= pressureFactor;
+ }
+ return HitTestPolygonSegment(_vertices, hitBegin, hitEnd);
+ }
+ else
+ {
+ // Iterate through the vertices of the contour of the ink segment
+ // check where the hitting segment is about them, return false if it's
+ // on the outer (left) side of the ink contour. This implementation might
+ // look more complex than straightforward separated hit-testing of three
+ // polygons (beginNode, quad, endNode), but it's supposed to be more optimal
+ // because the number of edges it hit-tests is approximately twice less
+ // than with the straightforward implementation.
+
+ // Start with the segment quad.C->quad.D
+ Vector hitBegin = hitBeginPoint - beginNode.Position;
+ Vector hitEnd = hitEndPoint - beginNode.Position;
+ HitResult hitResult = WhereIsSegmentAboutSegment(
+ hitBegin, hitEnd, quad.C - beginNode.Position, quad.D - beginNode.Position);
+ if (HitResult.Left == hitResult)
+ {
+ return false;
+ }
+
+ // Continue clockwise from quad.D to quad.C
+
+ HitResult firstResult = hitResult, lastResult = hitResult;
+ double pressureFactor = beginNode.PressureFactor;
+
+ // Find the index of the vertex that is quad.D
+ // Use count var to avoid infinite loop, normally it shouldn't
+ // happen but it doesn't hurt to check it just in case.
+ int i = 0, count = _vertices.Length;
+ Vector vertex = new Vector();
+ for (i = 0; i < count; i++)
+ {
+ vertex = _vertices[i] * pressureFactor;
+ // Here and in a few more places down the code, when comparing
+ // a quad's vertex vs a scaled shape vertex, it's important to
+ // compute them the same way as in GetConnectingQuad, so that not
+ // hit that double's computation error. For instance, sometimes the
+ // expression (vertex == quad.D - beginNode.Position) gives 'false'
+ // while the expression below gives 'true'. (Another workaround is to
+ // use DoubleUtil.AreClose but that;d be less performant)
+ if ((beginNode.Position + vertex) == quad.D)
+ {
+ break;
+ }
+ }
+ System.Diagnostics.Debug.Assert(count > 0);
+ // This loop does the iteration thru the edges of the ink segment
+ // clockwise from quad.D to quad.C.
+ for (int node = 0; node < 2; node++)
+ {
+ Point nodePosition = (node == 0) ? beginNode.Position : endNode.Position;
+ Point end = (node == 0) ? quad.A : quad.C;
+
+ count = _vertices.Length;
+ while (((nodePosition + vertex) != end) && (count != 0))
+ {
+ i = (i + 1) % _vertices.Length;
+ Vector nextVertex = (pressureFactor == 1) ? _vertices[i] : (_vertices[i] * pressureFactor);
+ hitResult = WhereIsSegmentAboutSegment(hitBegin, hitEnd, vertex, nextVertex);
+ if (HitResult.Hit == hitResult)
+ {
+ return true;
+ }
+ if (true == IsOutside(hitResult, lastResult))
+ {
+ return false;
+ }
+ lastResult = hitResult;
+ vertex = nextVertex;
+ count--;
+ }
+ System.Diagnostics.Debug.Assert(count > 0);
+
+ if (node == 0)
+ {
+ // The first iteration is done thru the outer segments of beginNode
+ // and ends at quad.A, for the second one make some adjustments
+ // to continue iterating through quad.AB and the outer segments of
+ // endNode up to quad.C
+ pressureFactor = endNode.PressureFactor;
+
+ Vector spineVector = endNode.Position - beginNode.Position;
+ vertex -= spineVector;
+ hitBegin -= spineVector;
+ hitEnd -= spineVector;
+
+ // Find the index of the vertex that is quad.B
+ count = _vertices.Length;
+ while (((endNode.Position + _vertices[i] * pressureFactor) != quad.B) && (count != 0))
+ {
+ i = (i + 1) % _vertices.Length;
+ count--;
+ }
+ System.Diagnostics.Debug.Assert(count > 0);
+ i--;
+ }
+ }
+ return (false == IsOutside(firstResult, hitResult));
+ }
+ }
+
+ ///
+ /// Hit-tests a stroke segment defined by two nodes against another stroke segment.
+ ///
+ /// Begin node of the stroke segment to hit-test. Can be empty (none)
+ /// End node of the stroke segment
+ /// Pre-computed quadrangle connecting the two nodes.
+ /// Can be empty if the begion node is empty or when one node is entirely inside the other
+ /// a collection of basic segments outlining the hitting contour
+ /// true if the contours intersect or overlap
+ internal virtual bool HitTest(
+ in StrokeNodeData beginNode, in StrokeNodeData endNode, Quad quad, IEnumerable hitContour)
+ {
+ // Check for special cases when the endNode is the very first one (beginNode.IsEmpty)
+ // or one node is completely inside the other. In either case the connecting quad
+ // would be Empty and we need to hittest against the biggest node (the one with
+ // the greater PressureFactor)
+ if (quad.IsEmpty)
+ {
+ // Make a call to hit-test the biggest node the hitting contour.
+ return HitTestPolygonContourSegments(hitContour, beginNode, endNode);
+ }
+ else
+ {
+ // HitTest the the hitting contour against the inking contour
+ return HitTestInkContour(hitContour, quad, beginNode, endNode);
+ }
+ }
+
+ ///
+ /// Hit-tests ink segment defined by two nodes against a linear segment.
+ ///
+ /// Begin node of the ink segment
+ /// End node of the ink segment
+ /// Pre-computed quadrangle connecting the two ink nodes
+ /// Begin point of the hitting segment
+ /// End point of the hitting segment
+ /// Exact location to cut at represented by StrokeFIndices
+ internal virtual StrokeFIndices CutTest(
+ in StrokeNodeData beginNode, in StrokeNodeData endNode, Quad quad, Point hitBeginPoint, Point hitEndPoint)
+ {
+ StrokeFIndices result = StrokeFIndices.Empty;
+
+ // First, find out if the hitting segment intersects with either of the ink nodes
+ for (int node = (beginNode.IsEmpty ? 1 : 0); node < 2; node++)
+ {
+ Point position = (node == 0) ? beginNode.Position : endNode.Position;
+ double pressureFactor = (node == 0) ? beginNode.PressureFactor : endNode.PressureFactor;
+
+ // Adjust the segment for the node's pressure factor
+ Vector hitBegin = hitBeginPoint - position;
+ Vector hitEnd = hitEndPoint - position;
+ if (pressureFactor != 1)
+ {
+ System.Diagnostics.Debug.Assert(DoubleUtil.IsZero(pressureFactor) == false);
+ hitBegin /= pressureFactor;
+ hitEnd /= pressureFactor;
+ }
+ // Hit-test the node against the segment
+ if (true == HitTestPolygonSegment(_vertices, hitBegin, hitEnd))
+ {
+ if (node == 0)
+ {
+ result.BeginFIndex = StrokeFIndices.BeforeFirst;
+ result.EndFIndex = 0;
+ }
+ else
+ {
+ result.EndFIndex = StrokeFIndices.AfterLast;
+ if (beginNode.IsEmpty)
+ {
+ result.BeginFIndex = StrokeFIndices.BeforeFirst;
+ }
+ else if (result.BeginFIndex != StrokeFIndices.BeforeFirst)
+ {
+ result.BeginFIndex = 1;
+ }
+ }
+ }
+ }
+
+ // If both nodes are hit, return.
+ if (result.IsFull)
+ {
+ return result;
+ }
+ // If there's no hit at all, return.
+ if (result.IsEmpty && (quad.IsEmpty || !HitTestQuadSegment(quad, hitBeginPoint, hitEndPoint)))
+ {
+ return result;
+ }
+
+ // The segments do intersect. Find findices on the ink segment to cut it at.
+ if (result.BeginFIndex != StrokeFIndices.BeforeFirst)
+ {
+ // The begin node is not hit, i.e. the begin findex is on this spine segment, find it.
+ result.BeginFIndex = ClipTest(
+ (endNode.Position - beginNode.Position) / beginNode.PressureFactor,
+ (endNode.PressureFactor / beginNode.PressureFactor) - 1,
+ (hitBeginPoint - beginNode.Position) / beginNode.PressureFactor,
+ (hitEndPoint - beginNode.Position) / beginNode.PressureFactor);
+ }
+
+ if (result.EndFIndex != StrokeFIndices.AfterLast)
+ {
+ // The end node is not hit, i.e. the end findex is on this spine segment, find it.
+ result.EndFIndex = 1 - ClipTest(
+ (beginNode.Position - endNode.Position) / endNode.PressureFactor,
+ (beginNode.PressureFactor / endNode.PressureFactor) - 1,
+ (hitBeginPoint - endNode.Position) / endNode.PressureFactor,
+ (hitEndPoint - endNode.Position) / endNode.PressureFactor);
+ }
+
+ if (IsInvalidCutTestResult(result))
+ {
+ return StrokeFIndices.Empty;
+ }
+
+ return result;
+ }
+
+ ///
+ /// CutTest
+ ///
+ /// Begin node of the stroke segment to hit-test. Can be empty (none)
+ /// End node of the stroke segment
+ /// Pre-computed quadrangle connecting the two nodes.
+ /// Can be empty if the begion node is empty or when one node is entirely inside the other
+ /// a collection of basic segments outlining the hitting contour
+ ///
+ internal virtual StrokeFIndices CutTest(
+ in StrokeNodeData beginNode, in StrokeNodeData endNode, Quad quad, IEnumerable hitContour)
+ {
+ if (beginNode.IsEmpty)
+ {
+ if (HitTest(beginNode, endNode, quad, hitContour) == true)
+ {
+ return StrokeFIndices.Full;
+ }
+ return StrokeFIndices.Empty;
+ }
+
+ StrokeFIndices result = StrokeFIndices.Empty;
+ bool isInside = true;
+ Vector spineVector = (endNode.Position - beginNode.Position) / beginNode.PressureFactor;
+ Vector spineVectorReversed = (beginNode.Position - endNode.Position) / endNode.PressureFactor;
+ double pressureDelta = (endNode.PressureFactor / beginNode.PressureFactor) - 1;
+ double pressureDeltaReversed = (beginNode.PressureFactor / endNode.PressureFactor) - 1;
+
+ foreach (ContourSegment hitSegment in hitContour)
+ {
+
+ // First, find out if hitSegment intersects with either of the ink nodes
+ bool isHit = HitTestStrokeNodes(hitSegment, beginNode, endNode, ref result);
+
+ // If both nodes are hit, return.
+ if (result.IsFull)
+ {
+ return result;
+ }
+
+ // If neither of the nodes is hit, hit-test the connecting quad
+ if (isHit == false)
+ {
+ // If neither of the nodes is hit and the contour of one node is entirely
+ // inside the contour of the other node, then done with this hitting segment
+ if (!quad.IsEmpty)
+ {
+ isHit = hitSegment.IsArc
+ ? HitTestQuadCircle(quad, hitSegment.Begin + hitSegment.Radius, hitSegment.Radius)
+ : HitTestQuadSegment(quad, hitSegment.Begin, hitSegment.End);
+ }
+
+ if (isHit == false)
+ {
+ if (isInside == true)
+ {
+ isInside = hitSegment.IsArc
+ ? (WhereIsVectorAboutArc(endNode.Position - hitSegment.Begin - hitSegment.Radius,
+ -hitSegment.Radius, hitSegment.Vector - hitSegment.Radius) != HitResult.Hit)
+ : (WhereIsVectorAboutVector(
+ endNode.Position - hitSegment.Begin, hitSegment.Vector) == HitResult.Right);
+ }
+ continue;
+ }
+ }
+
+ isInside = false;
+
+ // If the begin node is not hit, find the begin findex on the ink segment to cut it at
+ if (!DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst))
+ {
+ double findex = CalculateClipLocation(hitSegment, beginNode, spineVector, pressureDelta);
+ if (findex != StrokeFIndices.BeforeFirst)
+ {
+ System.Diagnostics.Debug.Assert(findex >= 0 && findex <= 1);
+ if (result.BeginFIndex > findex)
+ {
+ result.BeginFIndex = findex;
+ }
+ }
+ }
+
+ // If the end node is not hit, find the end findex on the ink segment to cut it at
+ if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ double findex = CalculateClipLocation(hitSegment, endNode, spineVectorReversed, pressureDeltaReversed);
+ if (findex != StrokeFIndices.BeforeFirst)
+ {
+ System.Diagnostics.Debug.Assert(findex >= 0 && findex <= 1);
+ findex = 1 - findex;
+ if (result.EndFIndex < findex)
+ {
+ result.EndFIndex = findex;
+ }
+ }
+ }
+ }
+
+ if (DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.AfterLast))
+ {
+ if (!DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.BeforeFirst))
+ {
+ result.BeginFIndex = StrokeFIndices.BeforeFirst;
+ }
+ }
+ else if (DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.BeforeFirst))
+ {
+ result.EndFIndex = StrokeFIndices.AfterLast;
+ }
+
+ if (IsInvalidCutTestResult(result))
+ {
+ return StrokeFIndices.Empty;
+ }
+
+ return (result.IsEmpty && isInside) ? StrokeFIndices.Full : result;
+ }
+
+ ///
+ /// Cutting ink with polygonal tip shapes with a linear segment
+ ///
+ /// Vector representing the starting and ending point for the inking
+ /// segment
+ /// Represents the difference in the node size for startNode and endNode.
+ /// pressureDelta = (endNode.PressureFactor / beginNode.PressureFactor) - 1
+ /// Start point of the hitting segment
+ /// End point of the hitting segment
+ /// a double representing the point of clipping
+ private double ClipTest(Vector spineVector, double pressureDelta, Vector hitBegin, Vector hitEnd)
+ {
+ // Let's represent the vertices for the startNode are N1, N2, ..., Ni and for the endNode, M1, M2,
+ // ..., Mi.
+ // When ink tip shape is a convex polygon, one may iterate in a constant direction
+ // (for instance, clockwise) through the edges of the polygon P1 and hit test the cutting segment
+ // against quadrangles NIMIMI+1NI+1 with MI on the left side off the vector NINI+1.
+ // If the cutting segment intersects the quadrangle, on the intersected part of the segment,
+ // one may then find point Q (the nearest to the line NINI+1) and point QP
+ // (the point of the intersection of the segment NIMI and vector NI+1NI started at Q).
+ // Next,
+ // QP = NI + s * LengthOf(MI - NI) (1)
+ // s = LengthOf(QP - NI ) / LengthOf(MI - NI). (2)
+ // If the cutting segment intersects more than one quadrant, one may then use the smallest s
+ // to find the split point:
+ // S = P1 + s * LengthOf(P2 - P1) (3)
+ double findex = StrokeFIndices.AfterLast;
+ Vector hitVector = hitEnd - hitBegin;
+ Vector lastVertex = _vertices[_vertices.Length - 1];
+
+ // Note the definition of pressureDelta = (endNode.PressureFactor / beginNode.PressureFactor) - 1
+ // So the equation below gives
+ // nextNode = spineVector + (endNode.PressureFactor / beginNode.PressureFactor)*lastVertex - lastVertex
+ // As a result, nextNode is a Vector pointing from lastVertex of the beginNode to the correspoinding "lastVertex"
+ // of the endNode.
+ Vector nextNode = spineVector + lastVertex * pressureDelta;
+ bool testNextEdge = false;
+
+ for (int k = 0, count = _vertices.Length; k < count || (k == count && testNextEdge); k++)
+ {
+ Vector vertex = _vertices[k % count];
+ Vector nextVertex = vertex - lastVertex;
+
+ // Point from vertex in beginNode to the corresponding "vertex" in endNode
+ Vector nextVertexNextNode = spineVector + (vertex * pressureDelta);
+
+ // Find out a "nextNode" on the endNode (nextNode) that is on the left side off the vector
+ // (lastVertex, vertex).
+ if ((DoubleUtil.IsZero(nextNode.X) && DoubleUtil.IsZero(nextNode.Y)) ||
+ (!testNextEdge && (HitResult.Left != WhereIsVectorAboutVector(nextNode, nextVertex))))
+ {
+ lastVertex = vertex;
+ nextNode = nextVertexNextNode;
+ continue;
+ }
+
+ // Now we need to do hit testing of the hitting segment against quarangle (NI, MI, MI+1, NI+1),
+ // that is, (lastVertex, nextNode, nextVertexNextNode, vertex)
+
+ testNextEdge = false;
+ HitResult hit = HitResult.Left;
+ int side = 0;
+ for (int i = 0; i < 2; i++)
+ {
+ Vector hitPoint = ((0 == i) ? hitBegin : hitEnd) - lastVertex;
+
+ hit = WhereIsVectorAboutVector(hitPoint, nextNode);
+ if (hit == HitResult.Hit)
+ {
+ double r = (Math.Abs(nextNode.X) < Math.Abs(nextNode.Y)) //DoubleUtil.IsZero(nextNode.X)
+ ? (hitPoint.Y / nextNode.Y)
+ : (hitPoint.X / nextNode.X);
+ if ((findex > r) && DoubleUtil.IsBetweenZeroAndOne(r))
+ {
+ findex = r;
+ }
+ }
+ else if (hit == HitResult.Right)
+ {
+ side++;
+ if (HitResult.Left == WhereIsVectorAboutVector(
+ hitPoint - nextVertex, nextVertexNextNode))
+ {
+ double r = GetPositionBetweenLines(nextVertex, nextNode, hitPoint);
+ if ((findex > r) && DoubleUtil.IsBetweenZeroAndOne(r))
+ {
+ findex = r;
+ }
+ }
+ else
+ {
+ testNextEdge = true;
+ }
+ }
+ else
+ {
+ side--;
+ }
+ }
+
+ //
+ if (0 == side)
+ {
+ if (hit == HitResult.Hit)
+ {
+ // This segment is collinear with the edge connecting the nodes,
+ // no need to hit-test the other edges.
+ System.Diagnostics.Debug.Assert(true == DoubleUtil.IsBetweenZeroAndOne(findex));
+ break;
+ }
+ // The hitting segment intersects the line of the edge connecting
+ // the nodes. Find the findex of the intersection point.
+ double det = -Vector.Determinant(nextNode, hitVector);
+ if (DoubleUtil.IsZero(det) == false)
+ {
+ double s = Vector.Determinant(hitVector, hitBegin - lastVertex) / det;
+ if ((findex > s) && DoubleUtil.IsBetweenZeroAndOne(s))
+ {
+ findex = s;
+ }
+ }
+ }
+ //
+ lastVertex = vertex;
+ nextNode = nextVertexNextNode;
+ }
+ return AdjustFIndex(findex);
+ }
+
+ ///
+ /// Clip-Testing a polygonal inking segment against an arc (circle)
+ ///
+ /// Vector representing the starting and ending point for the inking
+ /// segment
+ /// Represents the difference in the node size for startNode and endNode.
+ /// pressureDelta = (endNode.PressureFactor / beginNode.PressureFactor) - 1
+ /// The center of the hitting circle
+ /// The radius of the hitting circle
+ /// a double representing the point of clipping
+ private double ClipTestArc(Vector spineVector, double pressureDelta, Vector hitCenter, Vector hitRadius)
+ {
+ // this code is not called, but will be in VNext
+ throw new NotImplementedException();
+ /*
+ double findex = StrokeFIndices.AfterLast;
+
+ double radiusSquared = hitRadius.LengthSquared;
+ Vector vertex, lastVertex = _vertices[_vertices.Length - 1];
+ Vector nextVertexNextNode, nextNode = spineVector + lastVertex * pressureDelta;
+ bool testNextEdge = false;
+
+ for (int k = 0, count = _vertices.Length;
+ k < count || (k == count && testNextEdge);
+ k++, lastVertex = vertex, nextNode = nextVertexNextNode)
+ {
+ vertex = _vertices[k % count];
+ Vector nextVertex = vertex - lastVertex;
+ nextVertexNextNode = spineVector + (vertex * pressureDelta);
+
+ if (DoubleUtil.IsZero(nextNode.X) && DoubleUtil.IsZero(nextNode.Y))
+ {
+ continue;
+ }
+
+ bool testConnectingEdge = false;
+
+ if (HitResult.Left == WhereIsVectorAboutVector(nextNode, nextVertex))
+ {
+ testNextEdge = false;
+
+ Vector normal = GetProjection(lastVertex - hitCenter, vertex - hitCenter);
+ if (radiusSquared <= normal.LengthSquared)
+ {
+ if (WhereIsVectorAboutVector(hitCenter - lastVertex, nextVertex) == HitResult.Left)
+ {
+ Vector hitPoint = hitCenter + (normal * Math.Sqrt(radiusSquared / normal.LengthSquared));
+ if (HitResult.Right == WhereIsVectorAboutVector(hitPoint - vertex, nextVertexNextNode))
+ {
+ testNextEdge = true;
+ }
+ else if (HitResult.Left == WhereIsVectorAboutVector(hitPoint - lastVertex, nextNode))
+ {
+ testConnectingEdge = true;
+ }
+ else
+ {
+ // this is it
+ findex = GetPositionBetweenLines(nextVertex, nextNode, hitPoint - lastVertex);
+ System.Diagnostics.Debug.Assert(DoubleUtil.IsBetweenZeroAndOne(findex));
+ break;
+ }
+ }
+ }
+ else if (HitResult.Right == WhereIsVectorAboutVector(hitCenter + normal - lastVertex, nextNode))
+ {
+ testNextEdge = true;
+ }
+ else
+ {
+ testConnectingEdge = true;
+ }
+ }
+ else if (testNextEdge == true)
+ {
+ testNextEdge = false;
+ testConnectingEdge = true;
+ }
+
+ if (testConnectingEdge)
+ {
+ // Find out the projection of hitCenter on nextNode
+ Vector v = lastVertex - hitCenter;
+ double findexNearest = GetProjectionFIndex(v, v + nextNode);
+
+ if (findexNearest > 0)
+ {
+ Vector nearest = nextNode * findexNearest;
+ double squaredDistanceFromNearestToHitPoint = radiusSquared - (nearest + v).LengthSquared;
+ if (DoubleUtil.IsZero(squaredDistanceFromNearestToHitPoint) && (findexNearest <= 1))
+ {
+ if (findexNearest < findex)
+ {
+ findex = findexNearest;
+ }
+ }
+ else if ((squaredDistanceFromNearestToHitPoint > 0)
+ && (nearest.LengthSquared >= squaredDistanceFromNearestToHitPoint))
+ {
+ double hitPointFIndex = findexNearest - Math.Sqrt(
+ squaredDistanceFromNearestToHitPoint / nextNode.LengthSquared);
+ System.Diagnostics.Debug.Assert(DoubleUtil.GreaterThanOrClose(hitPointFIndex, 0));
+ if (hitPointFIndex < findex)
+ {
+ findex = hitPointFIndex;
+ }
+ }
+ }
+ }
+ }
+
+ return AdjustFIndex(findex);
+ */
+ }
+
+ ///
+ /// Internal access to __vertices
+ ///
+ ///
+ internal Vector[] GetVertices()
+ {
+ return _vertices;
+ }
+
+ ///
+ /// Helper function to hit-test the biggest node against hitting contour segments
+ ///
+ /// a collection of basic segments outlining the hitting contour
+ /// Begin node of the stroke segment to hit-test. Can be empty (none)
+ /// End node of the stroke segment
+ /// true if hit; false otherwise
+ private bool HitTestPolygonContourSegments(
+ IEnumerable hitContour, in StrokeNodeData beginNode, in StrokeNodeData endNode)
+ {
+ bool isHit = false;
+
+ // The bool variable isInside is used here to track that case. It answers to
+ // 'Is ink contour inside if the hitting contour?'. It's initialized to 'true"
+ // and then verified for each edge of the hitting contour until there's a hit or
+ // until it's false.
+ bool isInside = true;
+
+ Point position;
+ double pressureFactor;
+ if (beginNode.IsEmpty || endNode.PressureFactor > beginNode.PressureFactor)
+ {
+ position = endNode.Position;
+ pressureFactor = endNode.PressureFactor;
+ }
+ else
+ {
+ position = beginNode.Position;
+ pressureFactor = beginNode.PressureFactor;
+ }
+
+ // Enumerate through the segments of the hitting contour and test them
+ // one by one against the contour of the ink node.
+ foreach (ContourSegment hitSegment in hitContour)
+ {
+ if (hitSegment.IsArc)
+ {
+ // Adjust the arc for the node' pressure factor.
+ Vector hitCenter = hitSegment.Begin + hitSegment.Radius - position;
+ Vector hitRadius = hitSegment.Radius;
+ if (!DoubleUtil.AreClose(pressureFactor, 1d))
+ {
+ System.Diagnostics.Debug.Assert(DoubleUtil.IsZero(pressureFactor) == false);
+ hitCenter /= pressureFactor;
+ hitRadius /= pressureFactor;
+ }
+ // If the segment is an arc, hit-test against the entire circle the arc is part of.
+ if (true == HitTestPolygonCircle(_vertices, hitCenter, hitRadius))
+ {
+ isHit = true;
+ break;
+ }
+ //
+ if (isInside && (WhereIsVectorAboutArc(
+ position - hitSegment.Begin - hitSegment.Radius,
+ -hitSegment.Radius, hitSegment.Vector - hitSegment.Radius) == HitResult.Hit))
+ {
+ isInside = false;
+ }
+ }
+ else
+ {
+ // Adjust the segment for the node's pressure factor
+ Vector hitBegin = hitSegment.Begin - position;
+ Vector hitEnd = hitBegin + hitSegment.Vector;
+ if (!DoubleUtil.AreClose(pressureFactor, 1d))
+ {
+ System.Diagnostics.Debug.Assert(DoubleUtil.IsZero(pressureFactor) == false);
+ hitBegin /= pressureFactor;
+ hitEnd /= pressureFactor;
+ }
+ // Hit-test the node against the segment
+ if (true == HitTestPolygonSegment(_vertices, hitBegin, hitEnd))
+ {
+ isHit = true;
+ break;
+ }
+ //
+ if (isInside && WhereIsVectorAboutVector(
+ position - hitSegment.Begin, hitSegment.Vector) != HitResult.Right)
+ {
+ isInside = false;
+ }
+ }
+ }
+ return (isInside || isHit);
+ }
+
+ ///
+ /// Helper function to HitTest the the hitting contour against the inking contour
+ ///
+ /// a collection of basic segments outlining the hitting contour
+ /// A connecting quad
+ /// Begin node of the stroke segment to hit-test. Can be empty (none)
+ /// End node of the stroke segment
+ /// true if hit; false otherwise
+ private bool HitTestInkContour(
+ IEnumerable hitContour, Quad quad, in StrokeNodeData beginNode, in StrokeNodeData endNode)
+ {
+ System.Diagnostics.Debug.Assert(!quad.IsEmpty);
+ bool isHit = false;
+
+ // When hit-testing a contour against another contour, like in this case,
+ // the default implementation checks whether any edge (segment) of the hitting
+ // contour intersects with the contour of the ink segment. But this doesn't cover
+ // the case when the ink segment is entirely inside of the hitting segment.
+ // The bool variable isInside is used here to track that case. It answers to
+ // 'Is ink contour inside if the hitting contour?'. It's initialized to 'true"
+ // and then verified for each edge of the hitting contour until there's a hit or
+ // until it's false.
+ bool isInside = true;
+
+ // The ink connecting quad is not empty, enumerate through the segments of the
+ // hitting contour and hit-test them one by one against the ink contour.
+ foreach (ContourSegment hitSegment in hitContour)
+ {
+ // Iterate through the vertices of the contour of the ink segment
+ // check where the hit segment is about them, return false if it's
+ // on the left side off either of the ink contour segments.
+
+ Vector hitBegin, hitEnd;
+ HitResult hitResult;
+
+ // Start with the segment quad.C->quad.D
+ if (hitSegment.IsArc)
+ {
+ hitBegin = hitSegment.Begin + hitSegment.Radius - beginNode.Position;
+ hitEnd = hitSegment.Radius;
+ hitResult = WhereIsCircleAboutSegment(
+ hitBegin, hitEnd, quad.C - beginNode.Position, quad.D - beginNode.Position);
+ }
+ else
+ {
+ hitBegin = hitSegment.Begin - beginNode.Position;
+ hitEnd = hitBegin + hitSegment.Vector;
+ hitResult = WhereIsSegmentAboutSegment(
+ hitBegin, hitEnd, quad.C - beginNode.Position, quad.D - beginNode.Position);
+ }
+ if (HitResult.Left == hitResult)
+ {
+ if (isInside)
+ {
+ isInside = hitSegment.IsArc
+ ? (WhereIsVectorAboutArc(-hitBegin, -hitSegment.Radius, hitSegment.Vector - hitSegment.Radius) != HitResult.Hit)
+ : (WhereIsVectorAboutVector(-hitBegin, hitSegment.Vector) == HitResult.Right);
+ }
+ // This hitSegment is completely outside of the ink contour,
+ // continue with the next one.
+ continue;
+ }
+
+ // Continue clockwise from quad.D to quad.A, then to quad.B, ..., quad.C
+
+ HitResult firstResult = hitResult, lastResult = hitResult;
+ double pressureFactor = beginNode.PressureFactor;
+
+ // Find the index of the vertex that is quad.D
+ // Use count var to avoid infinite loop, normally this shouldn't
+ // happen but it doesn't hurt to check it just in case.
+ int i = 0, count = _vertices.Length;
+ Vector vertex = new Vector();
+ for (i = 0; i < count; i++)
+ {
+ vertex = _vertices[i] * pressureFactor;
+ if (DoubleUtil.AreClose((beginNode.Position + vertex), quad.D))
+ {
+ break;
+ }
+ }
+ System.Diagnostics.Debug.Assert(i < count);
+
+ int k;
+ for (k = 0; k < 2; k++)
+ {
+ count = _vertices.Length;
+ Point nodePosition = (k == 0) ? beginNode.Position : endNode.Position;
+ Point end = (k == 0) ? quad.A : quad.C;
+
+ // Iterate over the vertices on
+ // beginNode(k=0)from quad.D to quad.A
+ // or
+ // endNode(k=1)from quad.A to quad.B ... to quad.C
+ while (((nodePosition + vertex) != end) && (count != 0))
+ {
+ // Find out the next vertex
+ i = (i + 1) % _vertices.Length;
+ Vector nextVertex = _vertices[i] * pressureFactor;
+
+ // Hit-test the hitting segment against the current edge
+ hitResult = hitSegment.IsArc
+ ? WhereIsCircleAboutSegment(hitBegin, hitEnd, vertex, nextVertex)
+ : WhereIsSegmentAboutSegment(hitBegin, hitEnd, vertex, nextVertex);
+
+ if (HitResult.Hit == hitResult)
+ {
+ return true; //Got a hit
+ }
+ if (true == IsOutside(hitResult, lastResult))
+ {
+ // This hitSegment is definitely outside the ink contour, drop it.
+ // Change k to something > 2 to leave the for loop and skip
+ // IsOutside at the bottom
+ k = 3;
+ break;
+ }
+ lastResult = hitResult;
+ vertex = nextVertex;
+ count--;
+ }
+ System.Diagnostics.Debug.Assert(count > 0);
+
+ if (k == 0)
+ {
+ // Make some adjustments for the second one to continue iterating through
+ // quad.AB and the outer segments of endNode up to quad.C
+ pressureFactor = endNode.PressureFactor;
+ Vector spineVector = endNode.Position - beginNode.Position;
+ vertex -= spineVector; // now vertex = quad.A - spineVector
+ hitBegin -= spineVector; // adjust hitBegin to the space of endNode
+ if (hitSegment.IsArc == false)
+ {
+ hitEnd -= spineVector;
+ }
+
+ // Find the index of the vertex that is quad.B
+ count = _vertices.Length;
+ while (!DoubleUtil.AreClose((endNode.Position + _vertices[i] * pressureFactor), quad.B) && (count != 0))
+ {
+ i = (i + 1) % _vertices.Length;
+ count--;
+ }
+ System.Diagnostics.Debug.Assert(count > 0);
+ i--;
+ }
+ }
+ if ((k == 2) && (false == IsOutside(firstResult, hitResult)))
+ {
+ isHit = true;
+ break;
+ }
+ //
+ if (isInside)
+ {
+ isInside = hitSegment.IsArc
+ ? (WhereIsVectorAboutArc(-hitBegin, -hitSegment.Radius, hitSegment.Vector - hitSegment.Radius) != HitResult.Hit)
+ : (WhereIsVectorAboutVector(-hitBegin, hitSegment.Vector) == HitResult.Right);
+ }
+ }
+ return (isHit || isInside);
+ }
+
+
+ ///
+ /// Helper function to Hit-test against the two stroke nodes only (excluding the connecting quad).
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private bool HitTestStrokeNodes(
+ in ContourSegment hitSegment, in StrokeNodeData beginNode, in StrokeNodeData endNode, ref StrokeFIndices result)
+ {
+ // First, find out if hitSegment intersects with either of the ink nodes
+ bool isHit = false;
+ for (int node = 0; node < 2; node++)
+ {
+ Point position;
+ double pressureFactor;
+ if (node == 0)
+ {
+ if (isHit && DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst))
+ {
+ continue;
+ }
+ position = beginNode.Position;
+ pressureFactor = beginNode.PressureFactor;
+ }
+ else
+ {
+ if (isHit && DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ continue;
+ }
+ position = endNode.Position;
+ pressureFactor = endNode.PressureFactor;
+ }
+
+ Vector hitBegin, hitEnd;
+
+ // Adjust the segment for the node's pressure factor
+ if (hitSegment.IsArc)
+ {
+ hitBegin = hitSegment.Begin - position + hitSegment.Radius;
+ hitEnd = hitSegment.Radius;
+ }
+ else
+ {
+ hitBegin = hitSegment.Begin - position;
+ hitEnd = hitBegin + hitSegment.Vector;
+ }
+
+ if (pressureFactor != 1)
+ {
+ System.Diagnostics.Debug.Assert(DoubleUtil.IsZero(pressureFactor) == false);
+ hitBegin /= pressureFactor;
+ hitEnd /= pressureFactor;
+ }
+ // Hit-test the node against the segment
+ if (hitSegment.IsArc
+ ? HitTestPolygonCircle(_vertices, hitBegin, hitEnd)
+ : HitTestPolygonSegment(_vertices, hitBegin, hitEnd))
+ {
+ isHit = true;
+ if (node == 0)
+ {
+ result.BeginFIndex = StrokeFIndices.BeforeFirst;
+ if (DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ break;
+ }
+ }
+ else
+ {
+ result.EndFIndex = StrokeFIndices.AfterLast;
+ if (beginNode.IsEmpty)
+ {
+ result.BeginFIndex = StrokeFIndices.BeforeFirst;
+ break;
+ }
+ if (DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst))
+ {
+ break;
+ }
+ }
+ }
+ }
+ return isHit;
+ }
+
+ ///
+ /// Calculate the clip location
+ ///
+ /// the hitting segment
+ /// begin node
+ ///
+ ///
+ /// the clip location. not-clip if return StrokeFIndices.BeforeFirst
+ private double CalculateClipLocation(
+ in ContourSegment hitSegment, in StrokeNodeData beginNode, Vector spineVector, double pressureDelta)
+ {
+ double findex = StrokeFIndices.BeforeFirst;
+ bool clipIt = hitSegment.IsArc ? true
+ //? (WhereIsVectorAboutArc(beginNode.Position - hitSegment.Begin - hitSegment.Radius,
+ // -hitSegment.Radius, hitSegment.Vector - hitSegment.Radius) == HitResult.Hit)
+ : (WhereIsVectorAboutVector(
+ beginNode.Position - hitSegment.Begin, hitSegment.Vector) == HitResult.Left);
+ if (clipIt)
+ {
+ findex = hitSegment.IsArc
+ ? ClipTestArc(spineVector, pressureDelta,
+ (hitSegment.Begin + hitSegment.Radius - beginNode.Position) / beginNode.PressureFactor,
+ hitSegment.Radius / beginNode.PressureFactor)
+ : ClipTest(spineVector, pressureDelta,
+ (hitSegment.Begin - beginNode.Position) / beginNode.PressureFactor,
+ (hitSegment.End - beginNode.Position) / beginNode.PressureFactor);
+
+ // ClipTest returns StrokeFIndices.AfterLast to indicate a false hit test.
+ // But the caller CutTest expects StrokeFIndices.BeforeFirst when there is no hit.
+ if (findex == StrokeFIndices.AfterLast)
+ {
+ findex = StrokeFIndices.BeforeFirst;
+ }
+ else
+ {
+ System.Diagnostics.Debug.Assert(findex >= 0 && findex <= 1);
+ }
+ }
+ return findex;
+ }
+
+ ///
+ /// Helper method used to determine if we came up with a bogus result during hit testing
+ ///
+ protected bool IsInvalidCutTestResult(StrokeFIndices result)
+ {
+ //
+ // check for three invalid states
+ // 1) BeforeFirst == AfterLast
+ // 2) BeforeFirst, < 0
+ // 3) > 1, AfterLast
+ //
+ if (DoubleUtil.AreClose(result.BeginFIndex, result.EndFIndex) ||
+ DoubleUtil.AreClose(result.BeginFIndex, StrokeFIndices.BeforeFirst) && result.EndFIndex < 0.0f ||
+ result.BeginFIndex > 1.0f && DoubleUtil.AreClose(result.EndFIndex, StrokeFIndices.AfterLast))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ #endregion
+
+ #region Instance data
+
+ // Shape parameters
+ private Rect _shapeBounds = Rect.Empty;
+ protected Vector[] _vertices;
+
+ #endregion
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeOperations2.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeOperations2.cs
new file mode 100644
index 0000000..f55a717
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeNodeOperations2.cs
@@ -0,0 +1,579 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Input;
+using WpfInk.PresentationCore.System.Windows;
+
+namespace MS.Internal.Ink
+{
+ ///
+ /// Static methods implementing generic hit-testing operations
+ ///
+ internal partial class StrokeNodeOperations
+ {
+ #region enum HitResult
+
+ /// A set of possible results frequently used in StrokeNodeOperations and derived classes
+ internal enum HitResult
+ {
+ Hit,
+ Left,
+ Right,
+ InFront,
+ Behind
+ }
+
+ #endregion
+
+ #region HitTestXxxYyy
+
+ ///
+ /// Hit-tests a linear segment against a convex polygon.
+ ///
+ /// Vertices of the polygon (in clockwise order)
+ /// an end point of the hitting segment
+ /// an end point of the hitting segment
+ /// true if hit; false otherwise
+ internal static bool HitTestPolygonSegment(Vector[] vertices, Vector hitBegin, Vector hitEnd)
+ {
+ System.Diagnostics.Debug.Assert((null != vertices) && (2 < vertices.Length));
+
+ HitResult hitResult = HitResult.Right, firstResult = HitResult.Right, prevResult = HitResult.Right;
+ int count = vertices.Length;
+ Vector vertex = vertices[count - 1];
+ for (int i = 0; i < count; i++)
+ {
+ Vector nextVertex = vertices[i];
+ hitResult = WhereIsSegmentAboutSegment(hitBegin, hitEnd, vertex, nextVertex);
+ if (HitResult.Hit == hitResult)
+ {
+ return true;
+ }
+ if (IsOutside(hitResult, prevResult))
+ {
+ return false;
+ }
+ if (i == 0)
+ {
+ firstResult = hitResult;
+ }
+ prevResult = hitResult;
+ vertex = nextVertex;
+ }
+ return (false == IsOutside(firstResult, hitResult));
+ }
+
+ ///
+ /// This is a specialized version of HitTestPolygonSegment that takes
+ /// a Quad for a polygon. This method is called very intensively by
+ /// hit-testing API and we don't want to create Vector[] for every quad it hit-tests.
+ ///
+ /// the connecting quad to test against
+ /// begin point of the hitting segment
+ /// end point of the hitting segment
+ /// true if hit, false otherwise
+ internal static bool HitTestQuadSegment(Quad quad, Point hitBegin, Point hitEnd)
+ {
+ System.Diagnostics.Debug.Assert(quad.IsEmpty == false);
+
+ HitResult hitResult = HitResult.Right, firstResult = HitResult.Right, prevResult = HitResult.Right;
+ int count = 4;
+ Vector zeroVector = new Vector(0, 0);
+ Vector hitVector = hitEnd - hitBegin;
+ Vector vertex = quad[count - 1] - hitBegin;
+
+ for (int i = 0; i < count; i++)
+ {
+ Vector nextVertex = quad[i] - hitBegin;
+ hitResult = WhereIsSegmentAboutSegment(zeroVector, hitVector, vertex, nextVertex);
+ if (HitResult.Hit == hitResult)
+ {
+ return true;
+ }
+ if (true == IsOutside(hitResult, prevResult))
+ {
+ return false;
+ }
+ if (i == 0)
+ {
+ firstResult = hitResult;
+ }
+ prevResult = hitResult;
+ vertex = nextVertex;
+ }
+ return (false == IsOutside(firstResult, hitResult));
+ }
+
+ ///
+ /// Hit-test a polygin against a circle
+ ///
+ /// Vectors representing the vertices of the polygon, ordered in clockwise order
+ /// Vector representing the center of the circle
+ /// Vector representing the radius of the circle
+ /// true if hit, false otherwise
+ internal static bool HitTestPolygonCircle(Vector[] vertices, Vector center, Vector radius)
+ {
+ // this code is not called, but will be in VNext
+ throw new NotImplementedException();
+ /*
+ System.Diagnostics.Debug.Assert((null != vertices) && (2 < vertices.Length));
+
+ HitResult hitResult = HitResult.Right, firstResult = HitResult.Right, prevResult = HitResult.Right;
+ int count = vertices.Length;
+ Vector vertex = vertices[count - 1];
+
+ for (int i = 0; i < count; i++)
+ {
+ Vector nextVertex = vertices[i];
+ hitResult = WhereIsCircleAboutSegment(center, radius, vertex, nextVertex);
+ if (HitResult.Hit == hitResult)
+ {
+ return true;
+ }
+ if (true == IsOutside(hitResult, prevResult))
+ {
+ return false;
+ }
+ if (i == 0)
+ {
+ firstResult = hitResult;
+ }
+ prevResult = hitResult;
+ vertex = nextVertex;
+ }
+ return (false == IsOutside(firstResult, hitResult));
+ */
+ }
+
+ ///
+ /// This is a specialized version of HitTestPolygonCircle that takes
+ /// a Quad for a polygon. This method is called very intensively by
+ /// hit-testing API and we don't want to create Vector[] for every quad it hit-tests.
+ ///
+ /// the connecting quad
+ /// center of the circle
+ /// radius of the circle
+ /// true if hit; false otherwise
+ internal static bool HitTestQuadCircle(Quad quad, Point center, Vector radius)
+ {
+ // this code is not called, but will be in VNext
+ throw new NotImplementedException();
+ /*
+ System.Diagnostics.Debug.Assert(quad.IsEmpty == false);
+
+ Vector centerVector = (Vector)center;
+ HitResult hitResult = HitResult.Right, firstResult = HitResult.Right, prevResult = HitResult.Right;
+ int count = 4;
+ Vector vertex = (Vector)quad[count - 1];
+
+ for (int i = 0; i < count; i++)
+ {
+ Vector nextVertex = (Vector)quad[i];
+ hitResult = WhereIsCircleAboutSegment(centerVector, radius, vertex, nextVertex);
+ if (HitResult.Hit == hitResult)
+ {
+ return true;
+ }
+ if (true == IsOutside(hitResult, prevResult))
+ {
+ return false;
+ }
+ if (i == 0)
+ {
+ firstResult = hitResult;
+ }
+ prevResult = hitResult;
+ vertex = nextVertex;
+ }
+ return (false == IsOutside(firstResult, hitResult));
+ */
+ }
+
+ #endregion
+
+ #region Whereabouts
+
+ ///
+ /// Finds out where the segment [hitBegin, hitEnd]
+ /// is about the segment [orgBegin, orgEnd].
+ ///
+ internal static HitResult WhereIsSegmentAboutSegment(
+ Vector hitBegin, Vector hitEnd, Vector orgBegin, Vector orgEnd)
+ {
+ if (hitEnd == hitBegin)
+ {
+ return WhereIsCircleAboutSegment(hitBegin, new Vector(0, 0), orgBegin, orgEnd);
+ }
+
+ //----------------------------------------------------------------------
+ // Source: http://isc.faqs.org/faqs/graphics/algorithms-faq/
+ // Subject 1.03: How do I find intersections of 2 2D line segments?
+ //
+ // Let A,B,C,D be 2-space position vectors. Then the directed line
+ // segments AB & CD are given by:
+ //
+ // AB=A+r(B-A), r in [0,1]
+ // CD=C+s(D-C), s in [0,1]
+ //
+ // If AB & CD intersect, then
+ //
+ // A+r(B-A)=C+s(D-C), or Ax+r(Bx-Ax)=Cx+s(Dx-Cx)
+ // Ay+r(By-Ay)=Cy+s(Dy-Cy) for some r,s in [0,1]
+ //
+ // Solving the above for r and s yields
+ //
+ // (Ay-Cy)(Dx-Cx)-(Ax-Cx)(Dy-Cy)
+ // r = ----------------------------- (eqn 1)
+ // (Bx-Ax)(Dy-Cy)-(By-Ay)(Dx-Cx)
+ //
+ // (Ay-Cy)(Bx-Ax)-(Ax-Cx)(By-Ay)
+ // s = ----------------------------- (eqn 2)
+ // (Bx-Ax)(Dy-Cy)-(By-Ay)(Dx-Cx)
+ //
+ // Let P be the position vector of the intersection point, then
+ //
+ // P=A+r(B-A) or Px=Ax+r(Bx-Ax) and Py=Ay+r(By-Ay)
+ //
+ // By examining the values of r & s, you can also determine some
+ // other limiting conditions:
+ // If 0 <= r <= 1 && 0 <= s <= 1, intersection exists
+ // r < 0 or r > 1 or s < 0 or s > 1 line segments do not intersect
+ // If the denominator in eqn 1 is zero, AB & CD are parallel
+ // If the numerator in eqn 1 is also zero, AB & CD are collinear.
+ // If they are collinear, then the segments may be projected to the x-
+ // or y-axis, and overlap of the projected intervals checked.
+ //
+ // If the intersection point of the 2 lines are needed (lines in this
+ // context mean infinite lines) regardless whether the two line
+ // segments intersect, then
+ // If r > 1, P is located on extension of AB
+ // If r < 0, P is located on extension of BA
+ // If s > 1, P is located on extension of CD
+ // If s < 0, P is located on extension of DC
+ // Also note that the denominators of eqn 1 & 2 are identical.
+ //
+ // References:
+ // [O'Rourke (C)] pp. 249-51
+ // [Gems III] pp. 199-202 "Faster Line Segment Intersection,"
+ //----------------------------------------------------------------------
+ // The result tells where the segment CD is about the vector AB.
+ // Return "Right" if either C or D is not on the left from AB.
+ HitResult result = HitResult.Right;
+
+ // Calculate the vectors.
+ Vector AB = orgEnd - orgBegin; // B - A
+ Vector CA = orgBegin - hitBegin; // A - C
+ Vector CD = hitEnd - hitBegin; // D - C
+ double det = Vector.Determinant(AB, CD);
+
+ if (DoubleUtil.IsZero(det))
+ {
+ // The segments are parallel.
+ /*if (DoubleUtil.IsZero(Vector.Determinant(CD, CA)))
+ {
+ // The segments are collinear.
+ // Check if their X and Y projections overlap.
+ if ((Math.Max(orgBegin.X, orgEnd.X) >= Math.Min(hitBegin.X, hitEnd.X)) &&
+ (Math.Min(orgBegin.X, orgEnd.X) <= Math.Max(hitBegin.X, hitEnd.X)) &&
+ (Math.Max(orgBegin.Y, orgEnd.Y) >= Math.Min(hitBegin.Y, hitEnd.Y)) &&
+ (Math.Min(orgBegin.Y, orgEnd.Y) <= Math.Max(hitBegin.Y, hitEnd.Y)))
+ {
+ // The segments overlap.
+ result = HitResult.Hit;
+ }
+ else if (false == DoubleUtil.IsZero(AB.X))
+ {
+ result = ((AB.X * CA.X) > 0) ? HitResult.Behind : HitResult.InFront;
+ }
+ else
+ {
+ result = ((AB.Y * CA.Y) > 0) ? HitResult.Behind : HitResult.InFront;
+ }
+ }
+ else */
+ if (DoubleUtil.IsZero(Vector.Determinant(CD, CA)) || DoubleUtil.GreaterThan(Vector.Determinant(AB, CA), 0))
+ {
+ // C is on the left from AB, and, since the segments are parallel, D is also on the left.
+ result = HitResult.Left;
+ }
+ }
+ else
+ {
+ double r = AdjustFIndex(Vector.Determinant(AB, CA) / det);
+
+ if (r > 0 && r < 1)
+ {
+ // The line defined AB does cross the segment CD.
+ double s = AdjustFIndex(Vector.Determinant(CD, CA) / det);
+ if (s > 0 && s < 1)
+ {
+ // The crossing point is on the segment AB as well.
+ result = HitResult.Hit;
+ }
+ else
+ {
+ result = (0 < s) ? HitResult.InFront : HitResult.Behind;
+ }
+ }
+ else if ((WhereIsVectorAboutVector(hitBegin - orgBegin, AB) == HitResult.Left)
+ || (WhereIsVectorAboutVector(hitEnd - orgBegin, AB) == HitResult.Left))
+ {
+ // The line defined AB doesn't cross the segment CD, and neither C nor D
+ // is on the right from AB
+ result = HitResult.Left;
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Find out the relative location of a circle relative to a line segment
+ ///
+ /// center of the circle
+ /// radius of the circle. center.radius is a point on the circle
+ /// begin point of the line segment
+ /// end point of the line segment
+ /// test result
+ internal static HitResult WhereIsCircleAboutSegment(
+ Vector center, Vector radius, Vector segBegin, Vector segEnd)
+ {
+ segBegin -= center;
+ segEnd -= center;
+ double radiusSquared = radius.LengthSquared;
+
+ // This will find out the nearest path from center to a point on the segment
+ double distanceSquared = GetNearest(segBegin, segEnd).LengthSquared;
+
+ // The segment must cross the circle, hit
+ if (radiusSquared > distanceSquared)
+ {
+ return HitResult.Hit;
+ }
+
+ Vector segVector = segEnd - segBegin;
+ HitResult result = HitResult.Right;
+
+ // resolved two issues with the original code:
+ // 1. The local varial "normal" is assigned a value but it is never used afterwards. \
+ // 2. the code indicates that that only case result is HitResult.InFront or HitResult.Behind is
+ // when WhereIsVectorAboutVector(-segBegin, segVector) == HitResult.Left.
+
+ HitResult vResult = WhereIsVectorAboutVector(-segBegin, segVector);
+
+ //either front or behind
+ if (vResult == HitResult.Hit)
+ {
+ result = DoubleUtil.LessThan(segBegin.LengthSquared, segEnd.LengthSquared) ? HitResult.InFront :
+ HitResult.Behind;
+ }
+ else
+ {
+ // Find the projection of center on the segment.
+ double findex = GetProjectionFIndex(segBegin, segEnd);
+
+ // Get the normal vector, pointing from center to the projection point
+ Vector normal = segBegin + (segVector * findex);
+
+ // recalculate distanceSquared using normal
+ distanceSquared = normal.LengthSquared;
+
+ // The extension of the segment won't hit the circle
+ if (radiusSquared <= distanceSquared)
+ {
+ // either left or right
+ result = vResult;
+ }
+ else
+ {
+ result = (findex > 0) ? HitResult.InFront : HitResult.Behind;
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Finds out where the vector1 is about the vector2.
+ ///
+ internal static HitResult WhereIsVectorAboutVector(Vector vector1, Vector vector2)
+ {
+ double determinant = Vector.Determinant(vector1, vector2);
+ if (DoubleUtil.IsZero(determinant))
+ {
+ return HitResult.Hit; // collinear
+ }
+ return (0 < determinant) ? HitResult.Left : HitResult.Right;
+ }
+
+ ///
+ /// Tells whether the hitVector intersects the arc defined by two vectors.
+ ///
+ internal static HitResult WhereIsVectorAboutArc(Vector hitVector, Vector arcBegin, Vector arcEnd)
+ {
+ //HitResult result = HitResult.Right;
+ if (arcBegin == arcEnd)
+ {
+ // full circle
+ return HitResult.Hit;
+ }
+
+ if (HitResult.Right == WhereIsVectorAboutVector(arcEnd, arcBegin))
+ {
+ // small arc
+ if ((HitResult.Left != WhereIsVectorAboutVector(hitVector, arcBegin)) &&
+ (HitResult.Right != WhereIsVectorAboutVector(hitVector, arcEnd)))
+ {
+ return HitResult.Hit;
+ }
+ }
+ else if ((HitResult.Left != WhereIsVectorAboutVector(hitVector, arcBegin)) ||
+ (HitResult.Right != WhereIsVectorAboutVector(hitVector, arcEnd)))
+ {
+ return HitResult.Hit;
+ }
+
+ if ((WhereIsVectorAboutVector(hitVector - arcBegin, TurnLeft(arcBegin)) != HitResult.Left) ||
+ (WhereIsVectorAboutVector(hitVector - arcEnd, TurnRight(arcEnd)) != HitResult.Right))
+ {
+ return HitResult.Left;
+ }
+
+ return HitResult.Right;
+ }
+
+ #endregion
+
+ #region Misc. helpers
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static Vector TurnLeft(Vector vector)
+ {
+ // this code is not called, but will be in VNext
+ throw new NotImplementedException();
+ //return new Vector(-vector.Y, vector.X);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static Vector TurnRight(Vector vector)
+ {
+ // this code is not called, but will be in VNext
+ throw new NotImplementedException();
+ //return new Vector(vector.Y, -vector.X);
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ internal static bool IsOutside(HitResult hitResult, HitResult prevHitResult)
+ {
+ // ISSUE-2004/10/08-XiaoTu For Polygon and Circle, ((HitResult.Behind == hitResult) && (HitResult.InFront == prevHitResult))
+ // cannot be true.
+ return ((HitResult.Left == hitResult)
+ || ((HitResult.Behind == hitResult) && (HitResult.InFront == prevHitResult)));
+ }
+
+ ///
+ /// Internal helper function to find out the ratio of the distance from hitpoint to lineVector
+ /// and the distance from lineVector to (lineVector+nextLine)
+ ///
+ /// This is one edge of a polygonal node
+ /// The connection vector between the same edge on biginNode and ednNode
+ /// a point
+ /// the relative position of hitPoint
+ internal static double GetPositionBetweenLines(Vector linesVector, Vector nextLine, Vector hitPoint)
+ {
+ Vector nearestOnFirst = GetProjection(-hitPoint, linesVector - hitPoint);
+
+ hitPoint = nextLine - hitPoint;
+ Vector nearestOnSecond = GetProjection(hitPoint, hitPoint + linesVector);
+
+ Vector shortest = nearestOnFirst - nearestOnSecond;
+ System.Diagnostics.Debug.Assert((false == DoubleUtil.IsZero(shortest.X)) || (false == DoubleUtil.IsZero(shortest.Y)));
+
+ //return DoubleUtil.IsZero(shortest.X) ? (nearestOnFirst.Y / shortest.Y) : (nearestOnFirst.X / shortest.X);
+ return Math.Sqrt(nearestOnFirst.LengthSquared / shortest.LengthSquared);
+ }
+
+ ///
+ /// On a line defined buy two points finds the findex of the point
+ /// nearest to the origin (0,0). Same as FindNearestOnLine just
+ /// different output.
+ ///
+ /// A point on the line.
+ /// Another point on the line.
+ ///
+ internal static double GetProjectionFIndex(Vector begin, Vector end)
+ {
+ Vector segment = end - begin;
+ double lengthSquared = segment.LengthSquared;
+
+ if (DoubleUtil.IsZero(lengthSquared))
+ {
+ return 0;
+ }
+
+ double dotProduct = -(begin * segment);
+ return AdjustFIndex(dotProduct / lengthSquared);
+ }
+
+ ///
+ /// On a line defined buy two points finds the point nearest to the origin (0,0).
+ ///
+ /// A point on the line.
+ /// Another point on the line.
+ ///
+ internal static Vector GetProjection(Vector begin, Vector end)
+ {
+ double findex = GetProjectionFIndex(begin, end);
+ return (begin + (end - begin) * findex);
+ }
+
+ ///
+ /// On a given segment finds the point nearest to the origin (0,0).
+ ///
+ /// The segment's begin point.
+ /// The segment's end point.
+ ///
+ internal static Vector GetNearest(Vector begin, Vector end)
+ {
+ double findex = GetProjectionFIndex(begin, end);
+ if (findex <= 0)
+ {
+ return begin;
+ }
+ if (findex >= 1)
+ {
+ return end;
+ }
+ return (begin + ((end - begin) * findex));
+ }
+
+ ///
+ /// Clears double's computation fuzz around 0 and 1
+ ///
+ internal static double AdjustFIndex(double findex)
+ {
+ return DoubleUtil.IsZero(findex) ? 0 : (DoubleUtil.IsOne(findex) ? 1 : findex);
+ }
+
+ #endregion
+ }
+}
+
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeRenderer.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeRenderer.cs
new file mode 100644
index 0000000..a8c2128
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StrokeRenderer.cs
@@ -0,0 +1,1112 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+//#define DEBUG_RENDERING_FEEDBACK
+
+using System;
+using System.Collections.Generic;
+using System.Windows;
+using System.Windows.Input;
+using System.Diagnostics;
+
+using SRID = MS.Internal.PresentationCore.SRID;
+using MS.Internal;
+using MS.Internal.PresentationCore;
+using WpfInk;
+using WpfInk.PresentationCore.System.Windows;
+using WpfInk.PresentationCore.System.Windows.Ink;
+using WpfInk.WindowsBase.System.Windows.Media;
+
+namespace MS.Internal.Ink
+{
+ ///
+ /// An internal utility class that knows how to render a stroke
+ /// into an Avalon's DrawingContext.
+ ///
+ internal static class StrokeRenderer
+ {
+ #region Static API
+
+
+ ///
+ /// Calculate the StreamGeometry for the StrokeNodes.
+ /// This method is one of our most sensitive perf paths. It has been optimized to
+ /// create the minimum path figures in the StreamGeometry. There are two structures
+ /// we create for each point in a stroke, the strokenode and the connecting quad. Adding
+ /// strokenodes is very expensive later when MIL renders it, so this method has been optimized
+ /// to only add strokenodes when either pressure changes, or the angle of the stroke changes.
+ ///
+ public static void CalcGeometryAndBoundsWithTransform(StrokeNodeIterator iterator,
+ DrawingAttributes drawingAttributes,
+ MatrixTypes stylusTipMatrixType,
+ bool calculateBounds,
+ IInternalStreamGeometryContext context,
+ out Rect bounds)
+ {
+ Debug.Assert(iterator != null);
+ Debug.Assert(drawingAttributes != null);
+
+ //StreamGeometry streamGeometry = new StreamGeometry();
+ //streamGeometry.FillRule = FillRule.Nonzero;
+
+ //StreamGeometryContext context = streamGeometry.Open();
+ //geometry = streamGeometry;
+ bounds = Rect.Empty;
+ try
+ {
+ List connectingQuadPoints = new List(iterator.Count * 4);
+
+ //the index that the cb quad points are copied to
+ int cdIndex = iterator.Count * 2;
+ //the index that the ab quad points are copied to
+ int abIndex = 0;
+ for (int x = 0; x < cdIndex; x++)
+ {
+ //initialize so we can start copying to cdIndex later
+ connectingQuadPoints.Add(new Point(0d, 0d));
+ }
+
+ List strokeNodePoints = new List();
+ double lastAngle = 0.0d;
+ bool previousPreviousNodeRendered = false;
+
+ Rect lastRect = new Rect(0, 0, 0, 0);
+
+ for (int index = 0; index < iterator.Count; index++)
+ {
+ StrokeNode strokeNode = iterator[index];
+ System.Diagnostics.Debug.Assert(true == strokeNode.IsValid);
+
+ //the only code that calls this with !calculateBounds
+ //is dynamic rendering, which already draws enough strokeNodes
+ //to hide any visual artifacts.
+ //static rendering calculatesBounds, and we use those
+ //bounds below to figure out what angle to lay strokeNodes down for.
+ Rect strokeNodeBounds = strokeNode.GetBounds();
+ if (calculateBounds)
+ {
+ bounds.Union(strokeNodeBounds);
+ }
+
+ //if the angle between this and the last position has changed
+ //too much relative to the angle between the last+1 position and the last position
+ //we need to lay down stroke node
+ double delta = Math.Abs(GetAngleDeltaFromLast(strokeNode.PreviousPosition, strokeNode.Position, ref lastAngle));
+
+ double angleTolerance = 45d;
+ if (stylusTipMatrixType == MatrixTypes.TRANSFORM_IS_UNKNOWN)
+ {
+ //probably a skew is thrown in, we need to fall back to being very conservative
+ //about how many strokeNodes we prune
+ angleTolerance = 10d;
+ }
+ else if (strokeNodeBounds.Height > 40d || strokeNodeBounds.Width > 40d)
+ {
+ //if the strokeNode gets above a certain size, we need to lay down more strokeNodes
+ //to prevent visual artifacts
+ angleTolerance = 20d;
+ }
+ bool directionChanged = delta > angleTolerance && delta < (360d - angleTolerance);
+
+ double prevArea = lastRect.Height * lastRect.Width;
+ double currArea = strokeNodeBounds.Height * strokeNodeBounds.Width;
+ bool areaChangedOverThreshold = false;
+ if ((Math.Min(prevArea, currArea) / Math.Max(prevArea, currArea)) <= 0.70d)
+ {
+ //the min area is < 70% of the max area
+ areaChangedOverThreshold = true;
+ }
+
+ lastRect = strokeNodeBounds;
+
+ //render the stroke node for the first two nodes and last two nodes always
+ if (index <= 1 || index >= iterator.Count - 2 || directionChanged || areaChangedOverThreshold)
+ {
+ //special case... the direction has changed and we need to
+ //insert a stroke node in the StreamGeometry before we render the current one
+ if (directionChanged && !previousPreviousNodeRendered && index > 1 && index < iterator.Count - 1)
+ {
+ //insert a stroke node for the previous node
+ strokeNodePoints.Clear();
+ strokeNode.GetPreviousContourPoints(strokeNodePoints);
+ AddFigureToStreamGeometryContext(context, strokeNodePoints, strokeNode.IsEllipse/*isBezierFigure*/);
+
+ previousPreviousNodeRendered = true;
+ }
+
+ //render the stroke node
+ strokeNodePoints.Clear();
+ strokeNode.GetContourPoints(strokeNodePoints);
+ AddFigureToStreamGeometryContext(context, strokeNodePoints, strokeNode.IsEllipse/*isBezierFigure*/);
+ }
+
+ if (!directionChanged)
+ {
+ previousPreviousNodeRendered = false;
+ }
+
+ //add the end points of the connecting quad
+ Quad quad = strokeNode.GetConnectingQuad();
+ if (!quad.IsEmpty)
+ {
+ connectingQuadPoints[abIndex++] = quad.A;
+ connectingQuadPoints[abIndex++] = quad.B;
+ connectingQuadPoints.Add(quad.D);
+ connectingQuadPoints.Add(quad.C);
+ }
+
+ if (strokeNode.IsLastNode)
+ {
+ Debug.Assert(index == iterator.Count - 1);
+ if (abIndex > 0)
+ {
+ //we added something to the connecting quad points.
+ //now we need to do three things
+ //1) Shift the dc points down to the ab points
+ int cbStartIndex = iterator.Count * 2;
+ int cbEndIndex = connectingQuadPoints.Count - 1;
+ for (int i = abIndex, j = cbStartIndex; j <= cbEndIndex; i++, j++)
+ {
+ connectingQuadPoints[i] = connectingQuadPoints[j];
+ }
+
+ //2) trim the exess off the end of the array
+ int countToRemove = cbStartIndex - abIndex;
+ connectingQuadPoints.RemoveRange((cbEndIndex - countToRemove) + 1, countToRemove);
+
+ //3) reverse the dc points to make them cd points
+ for (int i = abIndex, j = connectingQuadPoints.Count - 1; i < j; i++, j--)
+ {
+ Point temp = connectingQuadPoints[i];
+ connectingQuadPoints[i] = connectingQuadPoints[j];
+ connectingQuadPoints[j] = temp;
+ }
+
+ //now render away!
+ AddFigureToStreamGeometryContext(context, connectingQuadPoints, false/*isBezierFigure*/);
+ }
+ }
+ }
+ }
+ finally
+ {
+
+ }
+ }
+
+
+ ///
+ /// Calculate the StreamGeometry for the StrokeNodes.
+ /// This method is one of our most sensitive perf paths. It has been optimized to
+ /// create the minimum path figures in the StreamGeometry. There are two structures
+ /// we create for each point in a stroke, the strokenode and the connecting quad. Adding
+ /// strokenodes is very expensive later when MIL renders it, so this method has been optimized
+ /// to only add strokenodes when either pressure changes, or the angle of the stroke changes.
+ ///
+ public static void CalcGeometryAndBounds(StrokeNodeIterator iterator,
+ DrawingAttributes drawingAttributes,
+#if DEBUG_RENDERING_FEEDBACK
+ DrawingContext debugDC,
+ double feedbackSize,
+ bool showFeedback,
+#endif
+ bool calculateBounds,
+ IInternalStreamGeometryContext context,
+ out Rect bounds)
+ {
+ Debug.Assert(iterator != null && drawingAttributes != null);
+
+ //we can use our new algorithm for identity only.
+ Matrix stylusTipTransform = drawingAttributes.StylusTipTransform;
+ if (stylusTipTransform != Matrix.Identity && stylusTipTransform._type != MatrixTypes.TRANSFORM_IS_SCALING)
+ {
+ //second best optimization
+ CalcGeometryAndBoundsWithTransform(iterator, drawingAttributes, stylusTipTransform._type, calculateBounds, context, out bounds);
+ }
+ else
+ {
+ //StreamGeometry streamGeometry = new StreamGeometry();
+ //streamGeometry.FillRule = FillRule.Nonzero;
+
+ //IStreamGeometryContext context = streamGeometry.Open();
+ //geometry = streamGeometry;
+ Rect empty = Rect.Empty;
+ bounds = empty;
+ try
+ {
+ //
+ // We keep track of three StrokeNodes as we iterate across
+ // the Stroke. Since these are structs, the default ctor will
+ // be called and .IsValid will be false until we initialize them
+ //
+ StrokeNode emptyStrokeNode = new StrokeNode();
+ StrokeNode prevPrevStrokeNode = new StrokeNode();
+ StrokeNode prevStrokeNode = new StrokeNode();
+ StrokeNode strokeNode = new StrokeNode();
+
+ Rect prevPrevStrokeNodeBounds = empty;
+ Rect prevStrokeNodeBounds = empty;
+ Rect strokeNodeBounds = empty;
+
+ //percentIntersect is a function of drawingAttributes height / width
+ double percentIntersect = 95d;
+ double maxExtent = Math.Max(drawingAttributes.Height, drawingAttributes.Width);
+ percentIntersect += Math.Min(4.99999d, ((maxExtent / 20d) * 5d));
+
+ double prevAngle = double.MinValue;
+ bool isStartOfSegment = true;
+ bool isEllipse = drawingAttributes.StylusTip == StylusTip.Ellipse;
+ bool ignorePressure = drawingAttributes.IgnorePressure;
+ //
+ // Two List's that get reused for adding figures
+ // to the streamgeometry.
+ //
+ List pathFigureABSide = new List();//don't prealloc. It causes Gen2 collections to rise and doesn't help execution time
+ List pathFigureDCSide = new List();
+ List polyLinePoints = new List(4);
+
+ int iteratorCount = iterator.Count;
+ for (int index = 0, previousIndex = -1; index < iteratorCount;)
+ {
+ if (!prevPrevStrokeNode.IsValid)
+ {
+ if (prevStrokeNode.IsValid)
+ {
+ //we're sliding our pointers forward
+ prevPrevStrokeNode = prevStrokeNode;
+ prevPrevStrokeNodeBounds = prevStrokeNodeBounds;
+ prevStrokeNode = emptyStrokeNode;
+ }
+ else
+ {
+ prevPrevStrokeNode = iterator[index++, previousIndex++];
+ prevPrevStrokeNodeBounds = prevPrevStrokeNode.GetBounds();
+ continue; //so we always check if index < iterator.Count
+ }
+ }
+
+ //we know prevPrevStrokeNode is valid
+ if (!prevStrokeNode.IsValid)
+ {
+ if (strokeNode.IsValid)
+ {
+ //we're sliding our pointers forward
+ prevStrokeNode = strokeNode;
+ prevStrokeNodeBounds = strokeNodeBounds;
+ strokeNode = emptyStrokeNode;
+ }
+ else
+ {
+ //get the next strokeNode, but don't automatically update previousIndex
+ prevStrokeNode = iterator[index++, previousIndex];
+ prevStrokeNodeBounds = prevStrokeNode.GetBounds();
+
+ RectCompareResult result =
+ FuzzyContains(prevStrokeNodeBounds,
+ prevPrevStrokeNodeBounds,
+ isStartOfSegment ? 99.99999d : percentIntersect);
+
+ if (result == RectCompareResult.Rect1ContainsRect2)
+ {
+ // this node already contains the prevPrevStrokeNodeBounds (PP):
+ //
+ // |------------|
+ // | |----| |
+ // | | PP | P |
+ // | |----| |
+ // |------------|
+ //
+ prevPrevStrokeNode = iterator[index - 1, prevPrevStrokeNode.Index - 1]; ;
+ prevPrevStrokeNodeBounds = Rect.Union(prevStrokeNodeBounds, prevPrevStrokeNodeBounds);
+
+ // at this point prevPrevStrokeNodeBounds already contains this node
+ // we can just ignore this node
+ prevStrokeNode = emptyStrokeNode;
+
+ // update previousIndex to point to this node
+ previousIndex = index - 1;
+
+ // go back to our main loop
+ continue;
+ }
+ else if (result == RectCompareResult.Rect2ContainsRect1)
+ {
+ // this prevPrevStrokeNodeBounds (PP) already contains this node:
+ //
+ // |------------|
+ // | |----||
+ // | PP | P ||
+ // | |----||
+ // |------------|
+ //
+
+ //prevPrevStrokeNodeBounds already contains this node
+ //we can just ignore this node
+ prevStrokeNode = emptyStrokeNode;
+
+ // go back to our main loop, but do not update previousIndex
+ // because it should continue to point to previousPrevious
+ continue;
+ }
+
+ Debug.Assert(!prevStrokeNode.GetConnectingQuad().IsEmpty, "prevStrokeNode.GetConnectingQuad() is Empty!");
+
+ // if neither was true, we now have two of our three nodes required to
+ // start our computation, we need to update previousIndex to point
+ // to our current, valid prevStrokeNode
+ previousIndex = index - 1;
+ continue; //so we always check if index < iterator.Count
+ }
+ }
+
+ //we know prevPrevStrokeNode and prevStrokeNode are both valid
+ if (!strokeNode.IsValid)
+ {
+ strokeNode = iterator[index++, previousIndex];
+ strokeNodeBounds = strokeNode.GetBounds();
+
+ RectCompareResult result =
+ FuzzyContains(strokeNodeBounds,
+ prevStrokeNodeBounds,
+ isStartOfSegment ? 99.99999 : percentIntersect);
+
+ RectCompareResult result2 =
+ FuzzyContains(strokeNodeBounds,
+ prevPrevStrokeNodeBounds,
+ isStartOfSegment ? 99.99999 : percentIntersect);
+
+ if (isStartOfSegment &&
+ result == RectCompareResult.Rect1ContainsRect2 &&
+ result2 == RectCompareResult.Rect1ContainsRect2)
+ {
+ if (pathFigureABSide.Count > 0)
+ {
+ //we've started a stroke, we need to end it before resetting
+ //prevPrev
+#if DEBUG_RENDERING_FEEDBACK
+ prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback);
+#else
+ prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide);
+#endif
+ //render
+ ReverseDCPointsRenderAndClear(context, pathFigureABSide, pathFigureDCSide, polyLinePoints, isEllipse, true/*clear the point collections*/);
+ }
+ //we're resetting
+ //prevPrevStrokeNode. We need to gen one
+ //without a connecting quad
+ prevPrevStrokeNode = iterator[index - 1, prevPrevStrokeNode.Index - 1];
+ prevPrevStrokeNodeBounds = prevPrevStrokeNode.GetBounds();
+ prevStrokeNode = emptyStrokeNode;
+ strokeNode = emptyStrokeNode;
+
+ // increment previousIndex to to point to this node
+ previousIndex = index - 1;
+ continue;
+ }
+ else if (result == RectCompareResult.Rect1ContainsRect2)
+ {
+ // this node (C) already contains the prevStrokeNodeBounds (P):
+ //
+ // |------------|
+ // |----| | |----| |
+ // | PP | | | P | C |
+ // |----| | |----| |
+ // |------------|
+ //
+ //we have to generate a new stroke node that points
+ //to pp since the connecting quad from C to P could be empty
+ //if they have the same point
+ strokeNode = iterator[index - 1, prevStrokeNode.Index - 1];
+ if (!strokeNode.GetConnectingQuad().IsEmpty)
+ {
+ //only update prevStrokeNode if we have a valid connecting quad
+ prevStrokeNode = strokeNode;
+ prevStrokeNodeBounds = Rect.Union(strokeNodeBounds, prevStrokeNodeBounds);
+
+ // update previousIndex, since it should point to this node now
+ previousIndex = index - 1;
+ }
+
+ // at this point we can just ignore this node
+ strokeNode = emptyStrokeNode;
+ //strokeNodeBounds = empty;
+
+ prevAngle = double.MinValue; //invalidate
+
+ // go back to our main loop
+ continue;
+ }
+ else if (result == RectCompareResult.Rect2ContainsRect1)
+ {
+ // this prevStrokeNodeBounds (P) already contains this node (C):
+ //
+ // |------------|
+ // |----| | |----||
+ // | PP | | P | C ||
+ // |----| | |----||
+ // |------------|
+ //
+ //prevStrokeNodeBounds already contains this node
+ //we can just ignore this node
+ strokeNode = emptyStrokeNode;
+
+ // go back to our main loop, but do not update previousIndex
+ // because it should continue to point to previous
+ continue;
+ }
+
+ Debug.Assert(!strokeNode.GetConnectingQuad().IsEmpty, "strokeNode.GetConnectingQuad was empty, this is unexpected");
+
+ //
+ // NOTE: we do not check if C contains PP, or PP contains C because
+ // that indicates a change in direction, which we handle below
+ //
+ // if neither was true P and C are separate,
+ // we now have all three nodes required to
+ // start our computation, we need to update previousIndex to point
+ // to our current, valid prevStrokeNode
+ previousIndex = index - 1;
+ }
+
+
+ // see if we have an overlap between the first and third node
+ bool overlap = prevPrevStrokeNodeBounds.IntersectsWith(strokeNodeBounds);
+
+ // prevPrevStrokeNode, prevStrokeNode and strokeNode are all
+ // valid nodes now. Now we need to figure out what do add to our
+ // PathFigure. First calc bounds on the strokeNode we know we need to render
+ if (calculateBounds)
+ {
+ bounds.Union(prevStrokeNodeBounds);
+ }
+
+ // determine what points to add to pathFigureABSide and pathFigureDCSide
+ // from prevPrevStrokeNode
+ if (pathFigureABSide.Count == 0)
+ {
+ Debug.Assert(pathFigureDCSide.Count == 0);
+ if (calculateBounds)
+ {
+ bounds.Union(prevPrevStrokeNodeBounds);
+ }
+
+ if (isStartOfSegment && overlap)
+ {
+ //render a complete first stroke node or we can get artifacts
+ prevPrevStrokeNode.GetContourPoints(polyLinePoints);
+ AddFigureToStreamGeometryContext(context, polyLinePoints, prevPrevStrokeNode.IsEllipse/*isBezierFigure*/);
+ polyLinePoints.Clear();
+ }
+
+ // we're starting a new pathfigure
+ // we need to add parts of the prevPrevStrokeNode contour
+ // to pathFigureABSide and pathFigureDCSide
+#if DEBUG_RENDERING_FEEDBACK
+ prevStrokeNode.GetPointsAtStartOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback);
+#else
+ prevStrokeNode.GetPointsAtStartOfSegment(pathFigureABSide, pathFigureDCSide);
+#endif
+
+ //set our marker, we're no longer at the start of the stroke
+ isStartOfSegment = false;
+ }
+
+
+
+ if (prevAngle == double.MinValue)
+ {
+ //prevAngle is no longer valid
+ prevAngle = GetAngleBetween(prevPrevStrokeNode.Position, prevStrokeNode.Position);
+ }
+ double delta = GetAngleDeltaFromLast(prevStrokeNode.Position, strokeNode.Position, ref prevAngle);
+ bool directionChangedOverAbsoluteThreshold = Math.Abs(delta) > 90d && Math.Abs(delta) < (360d - 90d);
+ bool directionChangedOverOverlapThreshold = overlap && !(ignorePressure || strokeNode.PressureFactor == 1f) && Math.Abs(delta) > 30d && Math.Abs(delta) < (360d - 30d);
+
+ double prevArea = prevStrokeNodeBounds.Height * prevStrokeNodeBounds.Width;
+ double currArea = strokeNodeBounds.Height * strokeNodeBounds.Width;
+
+ bool areaChanged = !(prevArea == currArea && prevArea == (prevPrevStrokeNodeBounds.Height * prevPrevStrokeNodeBounds.Width));
+ bool areaChangeOverThreshold = false;
+ if (overlap && areaChanged)
+ {
+ if ((Math.Min(prevArea, currArea) / Math.Max(prevArea, currArea)) <= 0.90d)
+ {
+ //the min area is < 70% of the max area
+ areaChangeOverThreshold = true;
+ }
+ }
+
+ if (areaChanged || delta != 0.0d || index >= iteratorCount)
+ {
+ //the area changed between the three nodes OR there was an angle delta OR we're at the end
+ //of the stroke... either way, this is a significant node. If not, we're going to drop it.
+ if ((overlap && (directionChangedOverOverlapThreshold || areaChangeOverThreshold)) ||
+ directionChangedOverAbsoluteThreshold)
+ {
+ //
+ // we need to stop the pathfigure at P
+ // and render the pathfigure
+ //
+ // |--| |--| |--||--| |------|
+ // |PP|------|P | |PP||P | |PP P C|
+ // |--| |--| |--||--| |------|
+ // / |C |
+ // |--| |--|
+ // |C |
+ // |--|
+
+
+#if DEBUG_RENDERING_FEEDBACK
+ prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback);
+#else
+ //end the figure
+ prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide);
+#endif
+ //render
+ ReverseDCPointsRenderAndClear(context, pathFigureABSide, pathFigureDCSide, polyLinePoints, isEllipse, true/*clear the point collections*/);
+
+ if (areaChangeOverThreshold)
+ {
+ //render a complete stroke node or we can get artifacts
+ prevStrokeNode.GetContourPoints(polyLinePoints);
+ AddFigureToStreamGeometryContext(context, polyLinePoints, prevStrokeNode.IsEllipse/*isBezierFigure*/);
+ polyLinePoints.Clear();
+ }
+ }
+ else
+ {
+ //
+ // direction didn't change over the threshold, add the midpoint data
+ // |--| |--|
+ // |PP|------|P |
+ // |--| |--|
+ // \
+ // |--|
+ // |C |
+ // |--|
+ bool endSegment; //flag that tell us if we missed an intersection
+#if DEBUG_RENDERING_FEEDBACK
+ strokeNode.GetPointsAtMiddleSegment(prevStrokeNode, delta, pathFigureABSide, pathFigureDCSide, out endSegment, debugDC, feedbackSize, showFeedback);
+#else
+ strokeNode.GetPointsAtMiddleSegment(prevStrokeNode, delta, pathFigureABSide, pathFigureDCSide, out endSegment);
+#endif
+ if (endSegment)
+ {
+ //we have a missing intersection, we need to end the
+ //segment at P
+#if DEBUG_RENDERING_FEEDBACK
+ prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback);
+#else
+ //end the figure
+ prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide);
+#endif
+ //render
+ ReverseDCPointsRenderAndClear(context, pathFigureABSide, pathFigureDCSide, polyLinePoints, isEllipse, true/*clear the point collections*/);
+ }
+ }
+ }
+
+ //
+ // either way... slide our pointers forward, to do this, we simply mark
+ // our first pointer as 'empty'
+ //
+ prevPrevStrokeNode = emptyStrokeNode;
+ prevPrevStrokeNodeBounds = empty;
+ }
+
+ //
+ // anything left to render?
+ //
+ if (prevPrevStrokeNode.IsValid)
+ {
+ if (prevStrokeNode.IsValid)
+ {
+ if (calculateBounds)
+ {
+ bounds.Union(prevPrevStrokeNodeBounds);
+ bounds.Union(prevStrokeNodeBounds);
+ }
+ Debug.Assert(!strokeNode.IsValid);
+ //
+ // we never made it to strokeNode, render two points, OR
+ // strokeNode was a dupe
+ //
+ if (pathFigureABSide.Count > 0)
+ {
+#if DEBUG_RENDERING_FEEDBACK
+ prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback);
+#else
+ //
+ // strokeNode was a dupe, we just need to render the end of the stroke
+ // which is at prevStrokeNode
+ //
+ prevStrokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide);
+#endif
+ //render
+ ReverseDCPointsRenderAndClear(context, pathFigureABSide, pathFigureDCSide, polyLinePoints, isEllipse, false/*clear the point collections*/);
+ }
+ else
+ {
+ // we've only seen two points to render
+ Debug.Assert(pathFigureDCSide.Count == 0);
+ //contains all the logic to render two stroke nodes
+ RenderTwoStrokeNodes(context,
+ prevPrevStrokeNode,
+ prevPrevStrokeNodeBounds,
+ prevStrokeNode,
+ prevStrokeNodeBounds,
+ pathFigureABSide,
+ pathFigureDCSide,
+ polyLinePoints
+#if DEBUG_RENDERING_FEEDBACK
+ ,debugDC,
+ feedbackSize,
+ showFeedback
+#endif
+ );
+ }
+ }
+ else
+ {
+ if (calculateBounds)
+ {
+ bounds.Union(prevPrevStrokeNodeBounds);
+ }
+
+ // we only have a single point to render
+ Debug.Assert(pathFigureABSide.Count == 0);
+ prevPrevStrokeNode.GetContourPoints(pathFigureABSide);
+ AddFigureToStreamGeometryContext(context, pathFigureABSide, prevPrevStrokeNode.IsEllipse/*isBezierFigure*/);
+ }
+ }
+ else if (prevStrokeNode.IsValid && strokeNode.IsValid)
+ {
+ if (calculateBounds)
+ {
+ bounds.Union(prevStrokeNodeBounds);
+ bounds.Union(strokeNodeBounds);
+ }
+
+ // typical case, we hit the end of the stroke
+ // see if we need to start a stroke, or just end one
+ if (pathFigureABSide.Count > 0)
+ {
+#if DEBUG_RENDERING_FEEDBACK
+ strokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide, debugDC, feedbackSize, showFeedback);
+#else
+ strokeNode.GetPointsAtEndOfSegment(pathFigureABSide, pathFigureDCSide);
+#endif
+
+ //render
+ ReverseDCPointsRenderAndClear(context, pathFigureABSide, pathFigureDCSide, polyLinePoints, isEllipse, false/*clear the point collections*/);
+
+ if (FuzzyContains(strokeNodeBounds, prevStrokeNodeBounds, 70d) != RectCompareResult.NoItersection)
+ {
+ //render a complete stroke node or we can get artifacts
+ strokeNode.GetContourPoints(polyLinePoints);
+ AddFigureToStreamGeometryContext(context, polyLinePoints, strokeNode.IsEllipse/*isBezierFigure*/);
+ }
+ }
+ else
+ {
+ Debug.Assert(pathFigureDCSide.Count == 0);
+ //contains all the logic to render two stroke nodes
+ RenderTwoStrokeNodes(context,
+ prevStrokeNode,
+ prevStrokeNodeBounds,
+ strokeNode,
+ strokeNodeBounds,
+ pathFigureABSide,
+ pathFigureDCSide,
+ polyLinePoints
+#if DEBUG_RENDERING_FEEDBACK
+ ,debugDC,
+ feedbackSize,
+ showFeedback
+#endif
+ );
+ }
+ }
+ }
+ finally
+ {
+ //context.Close();
+ //geometry.Freeze();
+ }
+ }
+ }
+
+
+ ///
+ /// Helper routine to render two distinct stroke nodes
+ ///
+ private static void RenderTwoStrokeNodes(IInternalStreamGeometryContext context,
+ StrokeNode strokeNodePrevious,
+ Rect strokeNodePreviousBounds,
+ StrokeNode strokeNodeCurrent,
+ Rect strokeNodeCurrentBounds,
+ List pointBuffer1,
+ List pointBuffer2,
+ List pointBuffer3
+#if DEBUG_RENDERING_FEEDBACK
+ ,DrawingContext debugDC,
+ double feedbackSize,
+ bool showFeedback
+#endif
+ )
+ {
+ Debug.Assert(pointBuffer1 != null);
+ Debug.Assert(pointBuffer2 != null);
+ Debug.Assert(pointBuffer3 != null);
+ Debug.Assert(context != null);
+
+
+ //see if we need to render a quad - if there is not at least a 70% overlap
+ if (FuzzyContains(strokeNodePreviousBounds, strokeNodeCurrentBounds, 70d) != RectCompareResult.NoItersection)
+ {
+ //we're between 100% and 70% overlapped
+ //just render two distinct figures with a connecting quad (if needed)
+ strokeNodePrevious.GetContourPoints(pointBuffer1);
+ AddFigureToStreamGeometryContext(context, pointBuffer1, strokeNodePrevious.IsEllipse/*isBezierFigure*/);
+
+ Quad quad = strokeNodeCurrent.GetConnectingQuad();
+ if (!quad.IsEmpty)
+ {
+ pointBuffer3.Add(quad.A);
+ pointBuffer3.Add(quad.B);
+ pointBuffer3.Add(quad.C);
+ pointBuffer3.Add(quad.D);
+ AddFigureToStreamGeometryContext(context, pointBuffer3, false/*isBezierFigure*/);
+ }
+
+ strokeNodeCurrent.GetContourPoints(pointBuffer2);
+ AddFigureToStreamGeometryContext(context, pointBuffer2, strokeNodeCurrent.IsEllipse/*isBezierFigure*/);
+ }
+ else
+ {
+ //we're less than 70% overlapped, it's safe to run our optimization
+#if DEBUG_RENDERING_FEEDBACK
+ strokeNodeCurrent.GetPointsAtStartOfSegment(pointBuffer1, pointBuffer2, debugDC, feedbackSize, showFeedback);
+ strokeNodeCurrent.GetPointsAtEndOfSegment(pointBuffer1, pointBuffer2, debugDC, feedbackSize, showFeedback);
+#else
+ strokeNodeCurrent.GetPointsAtStartOfSegment(pointBuffer1, pointBuffer2);
+ strokeNodeCurrent.GetPointsAtEndOfSegment(pointBuffer1, pointBuffer2);
+#endif
+ //render
+ ReverseDCPointsRenderAndClear(context, pointBuffer1, pointBuffer2, pointBuffer3, strokeNodeCurrent.IsEllipse, false/*clear the point collections*/);
+ }
+ }
+
+ ///
+ /// ReverseDCPointsRenderAndClear
+ ///
+ private static void ReverseDCPointsRenderAndClear(IInternalStreamGeometryContext context, List abPoints, List dcPoints, List polyLinePoints, bool isEllipse, bool clear)
+ {
+ //we need to reverse the cd side points
+ Point temp;
+ for (int i = 0, j = dcPoints.Count - 1; i < j; i++, j--)
+ {
+ temp = dcPoints[i];
+ dcPoints[i] = dcPoints[j];
+ dcPoints[j] = temp;
+ }
+ if (isEllipse)
+ {
+ AddArcToFigureToStreamGeometryContext(context, abPoints, dcPoints, polyLinePoints);
+ }
+ else
+ {
+ //for rectangles, render a single path figure by combining both sides
+ AddPolylineFigureToStreamGeometryContext(context, abPoints, dcPoints);
+ }
+
+ if (clear)
+ {
+ abPoints.Clear();
+ dcPoints.Clear();
+ }
+ }
+ ///
+ /// FuzzyContains for two rects
+ ///
+ private static RectCompareResult FuzzyContains(Rect rect1, Rect rect2, double percentIntersect)
+ {
+ Debug.Assert(percentIntersect >= 0.0 && percentIntersect <= 100.0d);
+
+
+ double intersectLeft = Math.Max(rect1.Left, rect2.Left);
+ double intersectTop = Math.Max(rect1.Top, rect2.Top);
+ double intersectWidth = Math.Max((double) (Math.Min(rect1.Right, rect2.Right) - intersectLeft), (double) 0);
+ double intersectHeight = Math.Max((double) (Math.Min(rect1.Bottom, rect2.Bottom) - intersectTop), (double) 0);
+
+ if (intersectWidth == 0.0d || intersectHeight == 0.0d)
+ {
+ return RectCompareResult.NoItersection;
+ }
+
+ //we have an intersection, see if it is enough
+ double rect1Area = rect1.Height * rect1.Width;
+ double rect2Area = rect2.Height * rect2.Width;
+ double minArea = Math.Min(rect1Area, rect2Area);
+ double intersectionArea = intersectWidth * intersectHeight;
+ double intersect = (intersectionArea / minArea) * 100d;
+ if (intersect >= percentIntersect)
+ {
+ if (rect1Area >= rect2Area)
+ {
+ return RectCompareResult.Rect1ContainsRect2;
+ }
+ return RectCompareResult.Rect2ContainsRect1;
+ }
+
+ return RectCompareResult.NoItersection;
+ }
+
+ ///
+ /// Private helper to render a path figure to the SGC
+ ///
+ private static void AddFigureToStreamGeometryContext(IInternalStreamGeometryContext context, List points, bool isBezierFigure)
+ {
+ Debug.Assert(context != null);
+ Debug.Assert(points != null);
+ Debug.Assert(points.Count > 0);
+
+ context.BeginFigure(points[points.Count - 1], //start point
+ true, //isFilled
+ true); //IsClosed
+
+ if (isBezierFigure)
+ {
+ context.PolyBezierTo(points,
+ true, //isStroked
+ true); //isSmoothJoin
+ }
+ else
+ {
+ context.PolyLineTo(points,
+ true, //isStroked
+ true); //isSmoothJoin
+ }
+ }
+
+
+ ///
+ /// Private helper to render a path figure to the SGC
+ ///
+ private static void AddPolylineFigureToStreamGeometryContext(IInternalStreamGeometryContext context, List abPoints, List dcPoints)
+ {
+ Debug.Assert(context != null);
+ Debug.Assert(abPoints != null && dcPoints != null);
+ Debug.Assert(abPoints.Count > 0 && dcPoints.Count > 0);
+
+ context.BeginFigure(abPoints[0], //start point
+ true, //isFilled
+ true); //IsClosed
+
+ context.PolyLineTo(abPoints,
+ true, //isStroked
+ true); //isSmoothJoin
+
+ context.PolyLineTo(dcPoints,
+ true, //isStroked
+ true); //isSmoothJoin
+ }
+
+ ///
+ /// Private helper to render a path figure to the SGC
+ ///
+ private static void AddArcToFigureToStreamGeometryContext(IInternalStreamGeometryContext context, List abPoints, List dcPoints, List polyLinePoints)
+ {
+ Debug.Assert(context != null);
+ Debug.Assert(abPoints != null && dcPoints != null);
+ Debug.Assert(polyLinePoints != null);
+ //Debug.Assert(abPoints.Count > 0 && dcPoints.Count > 0);
+ if (abPoints.Count == 0 || dcPoints.Count == 0)
+ {
+ return;
+ }
+
+ context.BeginFigure(abPoints[0], //start point
+ true, //isFilled
+ true); //IsClosed
+
+ for (int j = 0; j < 2; j++)
+ {
+ List points = j == 0 ? abPoints : dcPoints;
+ int startIndex = j == 0 ? 1 : 0;
+ for (int i = startIndex; i < points.Count;)
+ {
+ Point next = points[i];
+ if (next == StrokeRenderer.ArcToMarker)
+ {
+ if (polyLinePoints.Count > 0)
+ {
+ //polyline first
+ context.PolyLineTo(polyLinePoints,
+ true, //isStroked
+ true); //isSmoothJoin
+ polyLinePoints.Clear();
+ }
+ //we're arcing, pull out height, width and the arc to point
+ Debug.Assert(i + 2 < points.Count);
+ if (i + 2 < points.Count)
+ {
+ Point sizePoint = points[i + 1];
+ Size ellipseSize = new Size(sizePoint.X / 2/*width*/, sizePoint.Y / 2/*height*/);
+ Point arcToPoint = points[i + 2];
+
+ bool isLargeArc = false; //>= 180
+
+ context.ArcTo(arcToPoint,
+ ellipseSize,
+ 0d, //rotation
+ isLargeArc, //isLargeArc
+ sweepDirection: true, // SweepDirection.Clockwise
+ true, //isStroked
+ true); //isSmoothJoin
+ }
+ i += 3; //advance past this arcTo block
+ }
+ else
+ {
+ //walk forward until we find an arc marker or the end
+ polyLinePoints.Add(next);
+ i++;
+ }
+ }
+ if (polyLinePoints.Count > 0)
+ {
+ //polyline
+ context.PolyLineTo(polyLinePoints,
+ true, //isStroked
+ true); //isSmoothJoin
+ polyLinePoints.Clear();
+ }
+ }
+ }
+
+ ///
+ /// calculates the angle between the previousPosition and the current one and then computes the delta between
+ /// the lastAngle. lastAngle is also updated
+ ///
+ private static double GetAngleDeltaFromLast(Point previousPosition, Point currentPosition, ref double lastAngle)
+ {
+ double delta = 0.0d;
+
+ //input points typically come in very close to each other
+ double dx = (currentPosition.X * 1000) - (previousPosition.X * 1000);
+ double dy = (currentPosition.Y * 1000) - (previousPosition.Y * 1000);
+ if ((Int64) dx == 0 && (Int64) dy == 0)
+ {
+ //the points are close enough not to matter
+ //don't update lastAngle
+ return delta;
+ }
+
+ double angle = GetAngleBetween(previousPosition, currentPosition);
+
+ //special case when angle / lastAngle span 0 degrees
+ if (lastAngle >= 270 && angle <= 90)
+ {
+ delta = lastAngle - (360d + angle);
+ }
+ else if (lastAngle <= 90 && angle >= 270)
+ {
+ delta = (360d + lastAngle) - angle;
+ }
+ else
+ {
+ delta = (lastAngle - angle);
+ }
+ lastAngle = angle;
+
+ // Return
+ return delta;
+ }
+
+ ///
+ /// calculates the angle between the previousPosition and the current one and then computes the delta between
+ /// the lastAngle. lastAngle is also updated
+ ///
+ private static double GetAngleBetween(Point previousPosition, Point currentPosition)
+ {
+ double angle = 0.0d;
+
+ //input points typically come in very close to each other
+ double dx = (currentPosition.X * 1000) - (previousPosition.X * 1000);
+ double dy = (currentPosition.Y * 1000) - (previousPosition.Y * 1000);
+ if ((Int64) dx == 0 && (Int64) dy == 0)
+ {
+ //the points are close enough not to matter
+ return angle;
+ }
+
+ // Calculate angle
+ if (dx == 0.0)
+ {
+ if (dy == 0.0)
+ {
+ angle = 0.0;
+ }
+ else if (dy > 0.0)
+ {
+ angle = Math.PI / 2.0;
+ }
+ else
+ {
+ angle = Math.PI * 3.0 / 2.0;
+ }
+ }
+ else if (dy == 0.0)
+ {
+ if (dx > 0.0)
+ {
+ angle = 0.0;
+ }
+ else
+ {
+ angle = Math.PI;
+ }
+ }
+ else
+ {
+ if (dx < 0.0)
+ {
+ angle = Math.Atan(dy / dx) + Math.PI;
+ }
+ else if (dy < 0.0)
+ {
+ angle = Math.Atan(dy / dx) + (2 * Math.PI);
+ }
+ else
+ {
+ angle = Math.Atan(dy / dx);
+ }
+ }
+
+ // Convert to degrees
+ angle = angle * 180 / Math.PI;
+
+ // Return
+ return angle;
+ }
+
+ // Opacity for highlighter container visuals
+ internal static readonly double HighlighterOpacity = 0.5;
+ internal static readonly byte SolidStrokeAlpha = 0xFF;
+ internal static readonly Point ArcToMarker = new Point(Double.MinValue, Double.MinValue);
+
+ ///
+ /// Simple helper enum
+ ///
+ private enum RectCompareResult
+ {
+ Rect1ContainsRect2,
+ Rect2ContainsRect1,
+ NoItersection,
+ }
+ #endregion
+ }
+}
+
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StylusShape.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StylusShape.cs
new file mode 100644
index 0000000..acc1c64
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/MS/Internal/Ink/StylusShape.cs
@@ -0,0 +1,375 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using MS.Internal;
+using WpfInk.WindowsBase.System.Windows.Media;
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ ///
+ /// Defines the style of pen tip for rendering.
+ ///
+ ///
+ /// The Stylus size and coordinates are in units equal to 1/96th of an inch.
+ /// The default in V1 the default width is 1 pixel. This is 53 himetric units.
+ /// There are 2540 himetric units per inch.
+ /// This means that 53 high metric units is equivalent to 53/2540*96 in avalon.
+ ///
+ internal abstract class StylusShape
+ {
+ #region Fields
+
+ private double m_width;
+ private double m_height;
+ private double m_rotation;
+ private Point[] m_vertices;
+ private StylusTip m_tip;
+ private Matrix _transform = Matrix.Identity;
+
+ #endregion
+
+ #region Constructors
+
+ internal StylusShape() { }
+
+ ///
+ /// constructor for a StylusShape.
+ ///
+ internal StylusShape(StylusTip tip, double width, double height, double rotation)
+ {
+ if (Double.IsNaN(width) || Double.IsInfinity(width) || width < DrawingAttributes.MinWidth || width > DrawingAttributes.MaxWidth)
+ {
+ throw new ArgumentOutOfRangeException("width");
+ }
+
+ if (Double.IsNaN(height) || Double.IsInfinity(height) || height < DrawingAttributes.MinHeight || height > DrawingAttributes.MaxHeight)
+ {
+ throw new ArgumentOutOfRangeException("height");
+ }
+
+ if (Double.IsNaN(rotation) || Double.IsInfinity(rotation))
+ {
+ throw new ArgumentOutOfRangeException("rotation");
+ }
+
+ if (!StylusTipHelper.IsDefined(tip))
+ {
+ throw new ArgumentOutOfRangeException("tip");
+ }
+
+
+ //
+ // mod rotation to 360 (720 to 0, 361 to 1, -270 to 90)
+ //
+ m_width = width;
+ m_height = height;
+ m_rotation = rotation == 0 ? 0 : rotation % 360;
+ m_tip = tip;
+ if (tip == StylusTip.Rectangle)
+ {
+ ComputeRectangleVertices();
+ }
+ }
+
+ #endregion
+
+ #region Public properties
+
+ ///
+ /// Width of the non-rotated shape.
+ ///
+ public double Width { get { return m_width; } }
+
+ ///
+ /// Height of the non-rotated shape.
+ ///
+ public double Height { get { return m_height; } }
+
+ ///
+ /// The shape's rotation angle. The rotation is done about the origin (0,0).
+ ///
+ public double Rotation { get { return m_rotation; } }
+
+ ///
+ /// GetVerticesAsVectors
+ ///
+ ///
+ internal Vector[] GetVerticesAsVectors()
+ {
+ Vector[] vertices;
+
+ if (null != m_vertices)
+ {
+ // For a Rectangle
+ vertices = new Vector[m_vertices.Length];
+
+ if (_transform.IsIdentity)
+ {
+ for (int i = 0; i < vertices.Length; i++)
+ {
+ vertices[i] = (Vector) m_vertices[i];
+ }
+ }
+ else
+ {
+ for (int i = 0; i < vertices.Length; i++)
+ {
+ vertices[i] = _transform.Transform((Vector) m_vertices[i]);
+ }
+
+ // A transform might make the vertices in counter-clockwise order
+ // Fix it if this is the case.
+ FixCounterClockwiseVertices(vertices);
+ }
+ }
+ else
+ {
+ // For ellipse
+
+ // The transform is already applied on these points.
+ Point[] p = GetBezierControlPoints();
+ vertices = new Vector[p.Length];
+ for (int i = 0; i < vertices.Length; i++)
+ {
+ vertices[i] = (Vector) p[i];
+ }
+ }
+ return vertices;
+ }
+
+ #endregion
+
+ #region Misc. internal API
+
+ ///
+ /// This is the transform on the StylusShape
+ ///
+ internal Matrix Transform
+ {
+ get
+ {
+ return _transform;
+ }
+ set
+ {
+ global::System.Diagnostics.Debug.Assert(value.HasInverse);
+ _transform = value;
+ }
+ }
+
+ ///
+ /// A helper property.
+ ///
+ internal bool IsEllipse { get { return (null == m_vertices); } }
+
+ ///
+ /// A helper property.
+ ///
+ internal bool IsPolygon { get { return (null != m_vertices); } }
+
+ ///
+ /// Generally, there's no need for the shape's bounding box.
+ /// We use it to approximate v2 shapes with a rectangle for v1.
+ ///
+ internal Rect BoundingBox
+ {
+ get
+ {
+ Rect bbox;
+
+ if (this.IsPolygon)
+ {
+ bbox = Rect.Empty;
+ foreach (Point vertex in m_vertices)
+ {
+ bbox.Union(vertex);
+ }
+ }
+ // Future enhancement: Implement bbox for rotated ellipses.
+ else //if (DoubleUtil.IsZero(m_rotation) || DoubleUtil.AreClose(m_width, m_height))
+ {
+ bbox = new Rect(-(m_width * 0.5), -(m_height * 0.5), m_width, m_height);
+ }
+ //else
+ //{
+ // throw new NotImplementedException("Rotated ellipse");
+ //}
+
+ return bbox;
+ }
+ }
+ #endregion
+
+ #region Implementation helpers
+ /// TBS
+ private void ComputeRectangleVertices()
+ {
+ Point topLeft = new Point(-(m_width * 0.5), -(m_height * 0.5));
+ m_vertices = new Point[4] { topLeft,
+ topLeft + new Vector(m_width, 0),
+ topLeft + new Vector(m_width, m_height),
+ topLeft + new Vector(0, m_height)};
+ if (false == DoubleUtil.IsZero(m_rotation))
+ {
+ Matrix rotationTransform = Matrix.Identity;
+ rotationTransform.Rotate(m_rotation);
+ rotationTransform.Transform(m_vertices);
+ }
+ }
+
+
+ /// A transform might make the vertices in counter-clockwise order Fix it if this is the case.
+ private void FixCounterClockwiseVertices(Vector[] vertices)
+ {
+ // The private method should only called for Rectangle case.
+ global::System.Diagnostics.Debug.Assert(vertices.Length == 4);
+
+ Point prevVertex = (Point) vertices[vertices.Length - 1];
+ int counterClockIndex = 0, clockWiseIndex = 0;
+
+ for (int i = 0; i < vertices.Length; i++)
+ {
+ Point vertex = (Point) vertices[i];
+ Vector edge = vertex - prevVertex;
+
+ // Verify that the next vertex is on the right side off the edge vector.
+ double det = Vector.Determinant(edge, (Point) vertices[(i + 1) % vertices.Length] - (Point) vertex);
+ if (0 > det)
+ {
+ counterClockIndex++;
+ }
+ else if (0 < det)
+ {
+ clockWiseIndex++;
+ }
+
+ prevVertex = vertex;
+ }
+
+ // Assert the transform will make it either clockwise or counter-clockwise.
+ global::System.Diagnostics.Debug.Assert(clockWiseIndex == vertices.Length || counterClockIndex == vertices.Length);
+
+ if (counterClockIndex == vertices.Length)
+ {
+ // Make it Clockwise
+ int lastIndex = vertices.Length - 1;
+ for (int j = 0; j < vertices.Length / 2; j++)
+ {
+ Vector tmp = vertices[j];
+ vertices[j] = vertices[lastIndex - j];
+ vertices[lastIndex - j] = tmp;
+ }
+ }
+ }
+
+
+ private Point[] GetBezierControlPoints()
+ {
+ global::System.Diagnostics.Debug.Assert(m_tip == StylusTip.Ellipse);
+
+ // Approximating a 1/4 circle with a Bezier curve (borrowed from Avalon's EllipseGeometry.cs)
+ const double ArcAsBezier = 0.5522847498307933984; // =(\/2 - 1)*4/3
+
+ double radiusX = m_width / 2;
+ double radiusY = m_height / 2;
+ double borderMagicX = radiusX * ArcAsBezier;
+ double borderMagicY = radiusY * ArcAsBezier;
+
+ Point[] controlPoints = new Point[] {
+ new Point( -radiusX, -borderMagicY),
+ new Point(-borderMagicX, -radiusY),
+ new Point( 0, -radiusY),
+ new Point( borderMagicX, -radiusY),
+ new Point( radiusX, -borderMagicY),
+ new Point( radiusX, 0),
+ new Point( radiusX, borderMagicY),
+ new Point( borderMagicX, radiusY),
+ new Point( 0, radiusY),
+ new Point(-borderMagicX, radiusY),
+ new Point( -radiusX, borderMagicY),
+ new Point( -radiusX, 0)};
+
+ // Future enhancement: Apply the transform to the vertices
+ // Apply rotation and the shape transform to the control points
+ Matrix transform = Matrix.Identity;
+ if (m_rotation != 0)
+ {
+ transform.Rotate(m_rotation);
+ }
+
+ if (_transform.IsIdentity == false)
+ {
+ transform *= _transform;
+ }
+
+ if (transform.IsIdentity == false)
+ {
+ for (int i = 0; i < controlPoints.Length; i++)
+ {
+ controlPoints[i] = transform.Transform(controlPoints[i]);
+ }
+ }
+
+ return controlPoints;
+ }
+
+ #endregion
+ }
+
+ ///
+ /// Class for an elliptical StylusShape
+ ///
+ internal sealed class EllipseStylusShape : StylusShape
+ {
+ ///
+ /// Constructor for an elliptical StylusShape
+ ///
+ ///
+ ///
+ public EllipseStylusShape(double width, double height)
+ : this(width, height, 0f)
+ {
+ }
+
+ ///
+ /// Constructor for an ellptical StylusShape ,with roation in degree
+ ///
+ ///
+ ///
+ ///
+ public EllipseStylusShape(double width, double height, double rotation)
+ : base(StylusTip.Ellipse, width, height, rotation)
+ {
+ }
+ }
+
+ ///
+ /// Class for a rectangle StylusShape
+ ///
+ internal sealed class RectangleStylusShape : StylusShape
+ {
+ ///
+ /// Constructor
+ ///
+ ///
+ ///
+ public RectangleStylusShape(double width, double height)
+ : this(width, height, 0f)
+ {
+ }
+
+ ///
+ /// Constructor with rogation in degree
+ ///
+ ///
+ ///
+ ///
+ public RectangleStylusShape(double width, double height, double rotation)
+ : base(StylusTip.Rectangle, width, height, rotation)
+ {
+ }
+ }
+}
\ No newline at end of file
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/DrawingAttributes.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/DrawingAttributes.cs
new file mode 100644
index 0000000..2e99505
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/DrawingAttributes.cs
@@ -0,0 +1,979 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+
+using MS.Internal;
+
+using WpfInk.WindowsBase.System.Windows.Media;
+
+using SRID = MS.Internal.PresentationCore.SRID;
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ ///
+ /// DrawingAttributes is the list of attributes applied to an ink stroke
+ /// when it is drawn. The DrawingAttributes controls stroke color, width,
+ /// transparency, and more.
+ ///
+ ///
+ /// Note that when saving the DrawingAttributes, the V1 AntiAlias attribute
+ /// is always set, and on load the AntiAlias property is ignored.
+ ///
+ internal class DrawingAttributes : INotifyPropertyChanged
+ {
+ #region Constructors
+
+ ///
+ /// Creates a DrawingAttributes with default values
+ ///
+ public DrawingAttributes()
+ {
+ _extendedProperties = new ExtendedPropertyCollection();
+
+ Initialize();
+ }
+
+ ///
+ /// Common constructor call, also called by Clone
+ ///
+ private void Initialize()
+ {
+ Debug.Assert(_extendedProperties != null);
+ _extendedProperties.Changed +=
+ new ExtendedPropertiesChangedEventHandler(this.ExtendedPropertiesChanged_EventForwarder);
+ }
+
+ #endregion Constructors
+
+ #region Public Properties
+
+ ///
+ /// The StylusTip used to draw the stroke
+ ///
+ public StylusTip StylusTip
+ {
+ get
+ {
+ //prevent boxing / unboxing if possible
+ if (!_extendedProperties.Contains(KnownIds.StylusTip))
+ {
+ Debug.Assert(StylusTip.Ellipse == (StylusTip) GetDefaultDrawingAttributeValue(KnownIds.StylusTip));
+ return StylusTip.Ellipse;
+ }
+ else
+ {
+ //if we ever add to StylusTip enumeration, we need to just return GetExtendedPropertyBackedProperty
+ Debug.Assert(StylusTip.Rectangle == (StylusTip) GetExtendedPropertyBackedProperty(KnownIds.StylusTip));
+ return StylusTip.Rectangle;
+ }
+ }
+ set
+ {
+ //no need to raise change events, they will bubble up from the EPC
+ //underneath us
+ // Validation of value is done in EPC
+ SetExtendedPropertyBackedProperty(KnownIds.StylusTip, value);
+ }
+ }
+
+ ///
+ /// The StylusTip used to draw the stroke
+ ///
+ internal Matrix StylusTipTransform
+ {
+ get
+ {
+ //prevent boxing / unboxing if possible
+ if (!_extendedProperties.Contains(KnownIds.StylusTipTransform))
+ {
+ Debug.Assert(Matrix.Identity == (Matrix) GetDefaultDrawingAttributeValue(KnownIds.StylusTipTransform));
+ return Matrix.Identity;
+ }
+ return (Matrix) GetExtendedPropertyBackedProperty(KnownIds.StylusTipTransform);
+ }
+ set
+ {
+ Matrix m = (Matrix) value;
+ if (m.OffsetX != 0 || m.OffsetY != 0)
+ {
+ throw new ArgumentException(SR.Get(SRID.InvalidSttValue), "value");
+ }
+ //no need to raise change events, they will bubble up from the EPC
+ //underneath us
+ // Validation of value is done in EPC
+ SetExtendedPropertyBackedProperty(KnownIds.StylusTipTransform, value);
+ }
+ }
+
+ ///
+ /// The height of the StylusTip
+ ///
+ public double Height
+ {
+ get
+ {
+ //prevent boxing / unboxing if possible
+ if (!_extendedProperties.Contains(KnownIds.StylusHeight))
+ {
+ Debug.Assert(DrawingAttributes.DefaultHeight == (double) GetDefaultDrawingAttributeValue(KnownIds.StylusHeight));
+ return DrawingAttributes.DefaultHeight;
+ }
+ return (double) GetExtendedPropertyBackedProperty(KnownIds.StylusHeight);
+ }
+ set
+ {
+ if (double.IsNaN(value) || value < MinHeight || value > MaxHeight)
+ {
+ throw new ArgumentOutOfRangeException("Height", SR.Get(SRID.InvalidDrawingAttributesHeight));
+ }
+ //no need to raise change events, they will bubble up from the EPC
+ //underneath us
+ SetExtendedPropertyBackedProperty(KnownIds.StylusHeight, value);
+ }
+ }
+
+ ///
+ /// The width of the StylusTip
+ ///
+ public double Width
+ {
+ get
+ {
+ //prevent boxing / unboxing if possible
+ if (!_extendedProperties.Contains(KnownIds.StylusWidth))
+ {
+ Debug.Assert(DrawingAttributes.DefaultWidth == (double) GetDefaultDrawingAttributeValue(KnownIds.StylusWidth));
+ return DrawingAttributes.DefaultWidth;
+ }
+ return (double) GetExtendedPropertyBackedProperty(KnownIds.StylusWidth);
+ }
+ set
+ {
+ if (double.IsNaN(value) || value < MinWidth || value > MaxWidth)
+ {
+ throw new ArgumentOutOfRangeException("Width", SR.Get(SRID.InvalidDrawingAttributesWidth));
+ }
+ //no need to raise change events, they will bubble up from the EPC
+ //underneath us
+ SetExtendedPropertyBackedProperty(KnownIds.StylusWidth, value);
+ }
+ }
+
+ ///
+ /// When true, ink will be rendered as a series of curves instead of as
+ /// lines between Stylus sample points. This is useful for smoothing the ink, especially
+ /// when the person writing the ink has jerky or shaky writing.
+ /// This value is TRUE by default in the Avalon implementation
+ ///
+ public bool FitToCurve
+ {
+ get
+ {
+ DrawingFlags flags = (DrawingFlags) GetExtendedPropertyBackedProperty(KnownIds.DrawingFlags);
+ return (0 != (flags & DrawingFlags.FitToCurve));
+ }
+ set
+ {
+ //no need to raise change events, they will bubble up from the EPC
+ //underneath us
+ DrawingFlags flags = (DrawingFlags) GetExtendedPropertyBackedProperty(KnownIds.DrawingFlags);
+ if (value)
+ {
+ //turn on the bit
+ flags |= DrawingFlags.FitToCurve;
+ }
+ else
+ {
+ //turn off the bit
+ flags &= ~DrawingFlags.FitToCurve;
+ }
+ SetExtendedPropertyBackedProperty(KnownIds.DrawingFlags, flags);
+ }
+ }
+
+ ///
+ /// When true, ink will be rendered with any available pressure information
+ /// taken into account
+ ///
+ public bool IgnorePressure
+ {
+ get
+ {
+ DrawingFlags flags = (DrawingFlags) GetExtendedPropertyBackedProperty(KnownIds.DrawingFlags);
+ return (0 != (flags & DrawingFlags.IgnorePressure));
+ }
+ set
+ {
+ //no need to raise change events, they will bubble up from the EPC
+ //underneath us
+ DrawingFlags flags = (DrawingFlags) GetExtendedPropertyBackedProperty(KnownIds.DrawingFlags);
+ if (value)
+ {
+ //turn on the bit
+ flags |= DrawingFlags.IgnorePressure;
+ }
+ else
+ {
+ //turn off the bit
+ flags &= ~DrawingFlags.IgnorePressure;
+ }
+ SetExtendedPropertyBackedProperty(KnownIds.DrawingFlags, flags);
+ }
+ }
+
+ ///
+ /// Determines if the stroke should be treated as a highlighter
+ ///
+ public bool IsHighlighter
+ {
+ get
+ {
+ return false;
+ }
+ set
+ {
+
+ }
+ }
+
+ #region Extended Properties
+ ///
+ /// Allows addition of objects to the EPC
+ ///
+ ///
+ ///
+ public void AddPropertyData(Guid propertyDataId, object propertyData)
+ {
+ DrawingAttributes.ValidateStylusTipTransform(propertyDataId, propertyData);
+ SetExtendedPropertyBackedProperty(propertyDataId, propertyData);
+ }
+
+ ///
+ /// Allows removal of objects from the EPC
+ ///
+ ///
+ public void RemovePropertyData(Guid propertyDataId)
+ {
+ this.ExtendedProperties.Remove(propertyDataId);
+ }
+
+ ///
+ /// Allows retrieval of objects from the EPC
+ ///
+ ///
+ public object GetPropertyData(Guid propertyDataId)
+ {
+ return GetExtendedPropertyBackedProperty(propertyDataId);
+ }
+
+ ///
+ /// Allows retrieval of a Array of guids that are contained in the EPC
+ ///
+ public Guid[] GetPropertyDataIds()
+ {
+ return this.ExtendedProperties.GetGuidArray();
+ }
+
+ ///
+ /// Allows check of containment of objects to the EPC
+ ///
+ ///
+ public bool ContainsPropertyData(Guid propertyDataId)
+ {
+ return this.ExtendedProperties.Contains(propertyDataId);
+ }
+
+ ///
+ /// ExtendedProperties
+ ///
+ internal ExtendedPropertyCollection ExtendedProperties
+ {
+ get
+ {
+ return _extendedProperties;
+ }
+ }
+
+
+ /////
+ ///// Returns a copy of the EPC
+ /////
+ //internal ExtendedPropertyCollection CopyPropertyData()
+ //{
+ // return this.ExtendedProperties.Clone();
+ //}
+
+ #endregion
+
+
+
+ #endregion
+
+ #region Internal Properties
+
+ ///
+ /// StylusShape
+ ///
+ internal StylusShape StylusShape
+ {
+ get
+ {
+ StylusShape s;
+ if (this.StylusTip == StylusTip.Rectangle)
+ {
+ s = new RectangleStylusShape(this.Width, this.Height);
+ }
+ else
+ {
+ s = new EllipseStylusShape(this.Width, this.Height);
+ }
+
+ s.Transform = StylusTipTransform;
+ return s;
+ }
+ }
+
+ ///
+ /// Sets the Fitting error for this drawing attributes
+ ///
+ internal int FittingError
+ {
+ get
+ {
+ if (!_extendedProperties.Contains(KnownIds.CurveFittingError))
+ {
+ return 0;
+ }
+ else
+ {
+ return (int) _extendedProperties[KnownIds.CurveFittingError];
+ }
+ }
+ set
+ {
+ _extendedProperties[KnownIds.CurveFittingError] = value;
+ }
+ }
+
+ ///
+ /// Sets the Fitting error for this drawing attributes
+ ///
+ internal DrawingFlags DrawingFlags
+ {
+ get
+ {
+ return (DrawingFlags) GetExtendedPropertyBackedProperty(KnownIds.DrawingFlags);
+ }
+ set
+ {
+ //no need to raise change events, they will bubble up from the EPC
+ //underneath us
+ SetExtendedPropertyBackedProperty(KnownIds.DrawingFlags, value);
+ }
+ }
+
+ ///
+ /// When we load ISF from V1 if width is set and height is not
+ /// and PenTip is Circle, we need to set height to the same as width
+ /// or else we'll render different as an Ellipse. We use this flag to
+ /// preserve state for round tripping.
+ ///
+ internal bool HeightChangedForCompatabity
+ {
+ get { return _heightChangedForCompatabity; }
+ set { _heightChangedForCompatabity = value; }
+ }
+
+ #endregion
+
+ //------------------------------------------------------
+ //
+ // INotifyPropertyChanged Interface
+ //
+ //------------------------------------------------------
+
+ #region INotifyPropertyChanged Interface
+
+ ///
+ /// INotifyPropertyChanged.PropertyChanged event
+ ///
+ event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
+ {
+ add { _propertyChanged += value; }
+ remove { _propertyChanged -= value; }
+ }
+
+ #endregion INotifyPropertyChanged Interface
+
+ #region Methods
+
+ #region Object overrides
+
+ // What should ExtendedPropertyCollection.GetHashCode return?
+ /// Retrieve an integer-based value for using ExtendedPropertyCollection
+ /// objects in a hash table as keys
+ public override int GetHashCode()
+ {
+ return base.GetHashCode();
+ }
+
+ /// Overload of the Equals method which determines if two DrawingAttributes
+ /// objects contain the same drawing attributes
+ public override bool Equals(object o)
+ {
+ if (o == null || o.GetType() != this.GetType())
+ {
+ return false;
+ }
+
+ //use as and check for null instead of casting to DA to make presharp happy
+ DrawingAttributes that = o as DrawingAttributes;
+ if (that == null)
+ {
+ return false;
+ }
+
+ return (this._extendedProperties == that._extendedProperties);
+ }
+
+ /// Overload of the equality operator which determines
+ /// if two DrawingAttributes are equal
+ public static bool operator ==(DrawingAttributes first, DrawingAttributes second)
+ {
+ // compare the GC ptrs for the obvious reference equality
+ if (((object) first == null && (object) second == null) ||
+ ((object) first == (object) second))
+ {
+ return true;
+ }
+ // otherwise, if one of the ptrs are null, but not the other then return false
+ else if ((object) first == null || (object) second == null)
+ {
+ return false;
+ }
+ // finally use the full `blown value-style comparison against the collection contents
+ return first.Equals(second);
+ }
+
+ /// Overload of the not equals operator to determine if two
+ /// DrawingAttributes are different
+ public static bool operator !=(DrawingAttributes first, DrawingAttributes second)
+ {
+ return !(first == second);
+ }
+ #endregion
+
+ /////
+ ///// Copies the DrawingAttributes
+ /////
+ ///// Deep copy of the DrawingAttributes
+ /////
+ //public virtual DrawingAttributes Clone()
+ //{
+ // //
+ // // use MemberwiseClone, which will instance the most derived type
+ // // We use this instead of Activator.CreateInstance because it does not
+ // // require ReflectionPermission. One thing to note, all references
+ // // are shared, including event delegates, so we need to set those to null
+ // //
+ // DrawingAttributes clone = (DrawingAttributes) this.MemberwiseClone();
+
+ // //
+ // // null the delegates in the cloned DrawingAttributes
+ // //
+ // clone.AttributeChanged = null;
+ // clone.PropertyDataChanged = null;
+
+ // //make a copy of the epc , set up listeners
+ // clone._extendedProperties = _extendedProperties.Clone();
+ // clone.Initialize();
+
+ // //don't need to clone these, it is a value type
+ // //and is copied by MemberwiseClone
+ // //_v1RasterOperation
+ // //_heightChangedForCompatabity
+ // return clone;
+ //}
+ #endregion
+
+ #region Events
+
+ ///
+ /// Event fired whenever a DrawingAttribute is modified
+ ///
+ public event PropertyDataChangedEventHandler AttributeChanged;
+
+ ///
+ /// Method called when a change occurs to any DrawingAttribute
+ ///
+ /// The change information for the DrawingAttribute that was modified
+ protected virtual void OnAttributeChanged(PropertyDataChangedEventArgs e)
+ {
+ if (null == e)
+ {
+ throw new ArgumentNullException("e", SR.Get(SRID.EventArgIsNull));
+ }
+
+ try
+ {
+ PrivateNotifyPropertyChanged(e);
+ }
+ finally
+ {
+ if (this.AttributeChanged != null)
+ {
+ this.AttributeChanged(this, e);
+ }
+ }
+ }
+
+ ///
+ /// Event fired whenever a DrawingAttribute is modified
+ ///
+ public event PropertyDataChangedEventHandler PropertyDataChanged;
+
+ ///
+ /// Method called when a change occurs to any PropertyData
+ ///
+ /// The change information for the PropertyData that was modified
+ protected virtual void OnPropertyDataChanged(PropertyDataChangedEventArgs e)
+ {
+ if (null == e)
+ {
+ throw new ArgumentNullException("e", SR.Get(SRID.EventArgIsNull));
+ }
+
+ if (this.PropertyDataChanged != null)
+ {
+ this.PropertyDataChanged(this, e);
+ }
+ }
+
+
+ #endregion
+
+ #region Protected Methods
+
+ ///
+ /// Method called when a property change occurs to DrawingAttribute
+ ///
+ /// The EventArgs specifying the name of the changed property.
+ protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
+ {
+ if (_propertyChanged != null)
+ {
+ _propertyChanged(this, e);
+ }
+ }
+
+ #endregion Protected Methods
+
+ #region Private Methods
+
+ ///
+ /// Simple helper method used to determine if a guid
+ /// from an ExtendedProperty is used as the backing store
+ /// of a DrawingAttribute
+ ///
+ ///
+ ///
+ internal static object GetDefaultDrawingAttributeValue(Guid id)
+ {
+ if (KnownIds.Color == id)
+ {
+ //return Colors.Black;
+ }
+ if (KnownIds.StylusWidth == id)
+ {
+ return DrawingAttributes.DefaultWidth;
+ }
+ if (KnownIds.StylusTip == id)
+ {
+ return StylusTip.Ellipse;
+ }
+ if (KnownIds.DrawingFlags == id)
+ {
+ //note that in this implementation, FitToCurve is false by default
+ return DrawingFlags.AntiAliased;
+ }
+ if (KnownIds.StylusHeight == id)
+ {
+ return DrawingAttributes.DefaultHeight;
+ }
+ if (KnownIds.StylusTipTransform == id)
+ {
+ return Matrix.Identity;
+ }
+ if (KnownIds.IsHighlighter == id)
+ {
+ return false;
+ }
+ // this is a valid case
+ // as this helper method is used not only to
+ // get the default value, but also to see if
+ // the Guid is a drawing attribute value
+ return null;
+ }
+
+ internal static void ValidateStylusTipTransform(Guid propertyDataId, object propertyData)
+ {
+ //
+ // Calling AddPropertyData(KnownIds.StylusTipTransform, "d") does not throw an ArgumentException.
+ // ExtendedPropertySerializer.Validate take a string as a valid type since StylusTipTransform
+ // gets serialized as a String, but at runtime is a Matrix
+ if (propertyData == null)
+ {
+ throw new ArgumentNullException("propertyData");
+ }
+ else if (propertyDataId == KnownIds.StylusTipTransform)
+ {
+ // StylusTipTransform gets serialized as a String, but at runtime is a Matrix
+ Type t = propertyData.GetType();
+ if (t == typeof(String))
+ {
+ throw new ArgumentException(SR.Get(SRID.InvalidValueType, typeof(Matrix)), "propertyData");
+ }
+ }
+ }
+
+ ///
+ /// Simple helper method used to determine if a guid
+ /// needs to be removed from the ExtendedPropertyCollection in ISF
+ /// before serializing
+ ///
+ ///
+ ///
+ internal static bool RemoveIdFromExtendedProperties(Guid id)
+ {
+ if (KnownIds.Color == id ||
+ KnownIds.Transparency == id ||
+ KnownIds.StylusWidth == id ||
+ KnownIds.DrawingFlags == id ||
+ KnownIds.StylusHeight == id ||
+ KnownIds.CurveFittingError == id)
+ {
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Returns true if two DrawingAttributes lead to the same PathGeometry.
+ ///
+ internal static bool GeometricallyEqual(DrawingAttributes left, DrawingAttributes right)
+ {
+ // Optimization case:
+ // must correspond to the same path geometry if they refer to the same instance.
+ if (Object.ReferenceEquals(left, right))
+ {
+ return true;
+ }
+
+ if (left.StylusTip == right.StylusTip &&
+ left.StylusTipTransform == right.StylusTipTransform &&
+ DoubleUtil.AreClose(left.Width, right.Width) &&
+ DoubleUtil.AreClose(left.Height, right.Height) &&
+ left.DrawingFlags == right.DrawingFlags /*contains IgnorePressure / FitToCurve*/)
+ {
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Returns true if the guid passed in has impact on geometry of the stroke
+ ///
+ internal static bool IsGeometricalDaGuid(Guid guid)
+ {
+ // Assert it is a DA guid
+ Debug.Assert(null != DrawingAttributes.GetDefaultDrawingAttributeValue(guid));
+
+ if (guid == KnownIds.StylusHeight || guid == KnownIds.StylusWidth ||
+ guid == KnownIds.StylusTipTransform || guid == KnownIds.StylusTip ||
+ guid == KnownIds.DrawingFlags)
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+
+
+ ///
+ /// Whenever the base class fires the generic ExtendedPropertiesChanged
+ /// event, we need to fire the DrawingAttributesChanged event also.
+ ///
+ /// Should be 'this' object
+ /// The custom attributes that changed
+ private void ExtendedPropertiesChanged_EventForwarder(object sender, ExtendedPropertiesChangedEventArgs args)
+ {
+ Debug.Assert(sender != null);
+ Debug.Assert(args != null);
+
+ //see if the EP that changed is a drawingattribute
+ if (args.NewProperty == null)
+ {
+ //a property was removed, see if it is a drawing attribute property
+ object defaultValueIfDrawingAttribute
+ = DrawingAttributes.GetDefaultDrawingAttributeValue(args.OldProperty.Id);
+ if (defaultValueIfDrawingAttribute != null)
+ {
+ ExtendedProperty newProperty =
+ new ExtendedProperty(args.OldProperty.Id,
+ defaultValueIfDrawingAttribute);
+ //this is a da guid
+ PropertyDataChangedEventArgs dargs =
+ new PropertyDataChangedEventArgs(args.OldProperty.Id,
+ newProperty.Value, //the property
+ args.OldProperty.Value);//previous value
+
+ this.OnAttributeChanged(dargs);
+ }
+ else
+ {
+ PropertyDataChangedEventArgs dargs =
+ new PropertyDataChangedEventArgs(args.OldProperty.Id,
+ null, //the property
+ args.OldProperty.Value);//previous value
+
+ this.OnPropertyDataChanged(dargs);
+ }
+ }
+ else if (args.OldProperty == null)
+ {
+ //a property was added, see if it is a drawing attribute property
+ object defaultValueIfDrawingAttribute
+ = DrawingAttributes.GetDefaultDrawingAttributeValue(args.NewProperty.Id);
+ if (defaultValueIfDrawingAttribute != null)
+ {
+ if (!defaultValueIfDrawingAttribute.Equals(args.NewProperty.Value))
+ {
+ //this is a da guid
+ PropertyDataChangedEventArgs dargs =
+ new PropertyDataChangedEventArgs(args.NewProperty.Id,
+ args.NewProperty.Value, //the property
+ defaultValueIfDrawingAttribute); //previous value
+
+ this.OnAttributeChanged(dargs);
+ }
+ }
+ else
+ {
+ PropertyDataChangedEventArgs dargs =
+ new PropertyDataChangedEventArgs(args.NewProperty.Id,
+ args.NewProperty.Value, //the property
+ null); //previous value
+ this.OnPropertyDataChanged(dargs);
+ }
+ }
+ else
+ {
+ //something was modified, see if it is a drawing attribute property
+ object defaultValueIfDrawingAttribute
+ = DrawingAttributes.GetDefaultDrawingAttributeValue(args.NewProperty.Id);
+ if (defaultValueIfDrawingAttribute != null)
+ {
+ //
+ // we only raise DA changed when the value actually changes
+ //
+ if (!args.NewProperty.Value.Equals(args.OldProperty.Value))
+ {
+ //this is a da guid
+ PropertyDataChangedEventArgs dargs =
+ new PropertyDataChangedEventArgs(args.NewProperty.Id,
+ args.NewProperty.Value, //the da
+ args.OldProperty.Value);//old value
+
+ this.OnAttributeChanged(dargs);
+ }
+ }
+ else
+ {
+ if (!args.NewProperty.Value.Equals(args.OldProperty.Value))
+ {
+ PropertyDataChangedEventArgs dargs =
+ new PropertyDataChangedEventArgs(args.NewProperty.Id,
+ args.NewProperty.Value,
+ args.OldProperty.Value);//old value
+
+ this.OnPropertyDataChanged(dargs);
+ }
+ }
+ }
+ }
+
+ ///
+ /// All DrawingAttributes are backed by an ExtendedProperty
+ /// this is a simple helper to set a property
+ ///
+ /// id
+ /// value
+ private void SetExtendedPropertyBackedProperty(Guid id, object value)
+ {
+ if (_extendedProperties.Contains(id))
+ {
+ //
+ // check to see if we're setting the property back
+ // to a default value. If we are we should remove it from
+ // the EPC
+ //
+ object defaultValue = DrawingAttributes.GetDefaultDrawingAttributeValue(id);
+ if (defaultValue != null)
+ {
+ if (defaultValue.Equals(value))
+ {
+ _extendedProperties.Remove(id);
+ return;
+ }
+ }
+ //
+ // we're setting a non-default value on a EP that
+ // already exists, check for equality before we do
+ // so we don't raise unnecessary EPC changed events
+ //
+ object o = GetExtendedPropertyBackedProperty(id);
+ if (!o.Equals(value))
+ {
+ _extendedProperties[id] = value;
+ }
+ }
+ else
+ {
+ //
+ // make sure we're not setting a default value of the guid
+ // there is no need to do this
+ //
+ object defaultValue = DrawingAttributes.GetDefaultDrawingAttributeValue(id);
+ if (defaultValue == null || !defaultValue.Equals(value))
+ {
+ _extendedProperties[id] = value;
+ }
+ }
+ }
+
+ ///
+ /// All DrawingAttributes are backed by an ExtendedProperty
+ /// this is a simple helper to set a property
+ ///
+ /// id
+ ///
+ private object GetExtendedPropertyBackedProperty(Guid id)
+ {
+ if (!_extendedProperties.Contains(id))
+ {
+ if (null != DrawingAttributes.GetDefaultDrawingAttributeValue(id))
+ {
+ return DrawingAttributes.GetDefaultDrawingAttributeValue(id);
+ }
+ throw new ArgumentException(SR.Get(SRID.EPGuidNotFound), "id");
+ }
+ else
+ {
+ return _extendedProperties[id];
+ }
+ }
+
+ ///
+ /// A help method which fires INotifyPropertyChanged.PropertyChanged event
+ ///
+ ///
+ private void PrivateNotifyPropertyChanged(PropertyDataChangedEventArgs e)
+ {
+ if (e.PropertyGuid == KnownIds.Color)
+ {
+ OnPropertyChanged("Color");
+ }
+ else if (e.PropertyGuid == KnownIds.StylusTip)
+ {
+ OnPropertyChanged("StylusTip");
+ }
+ else if (e.PropertyGuid == KnownIds.StylusTipTransform)
+ {
+ OnPropertyChanged("StylusTipTransform");
+ }
+ else if (e.PropertyGuid == KnownIds.StylusHeight)
+ {
+ OnPropertyChanged("Height");
+ }
+ else if (e.PropertyGuid == KnownIds.StylusWidth)
+ {
+ OnPropertyChanged("Width");
+ }
+ else if (e.PropertyGuid == KnownIds.IsHighlighter)
+ {
+ OnPropertyChanged("IsHighlighter");
+ }
+ else if (e.PropertyGuid == KnownIds.DrawingFlags)
+ {
+ DrawingFlags changedBits = (((DrawingFlags) e.PreviousValue) ^ ((DrawingFlags) e.NewValue));
+
+ // NOTICE-2006/01/20-WAYNEZEN,
+ // If someone changes FitToCurve and IgnorePressure simultaneously via AddPropertyData/RemovePropertyData,
+ // we will fire both OnPropertyChangeds in advance the order of the values.
+ if ((changedBits & DrawingFlags.FitToCurve) != 0)
+ {
+ OnPropertyChanged("FitToCurve");
+ }
+
+ if ((changedBits & DrawingFlags.IgnorePressure) != 0)
+ {
+ OnPropertyChanged("IgnorePressure");
+ }
+ }
+ }
+
+ private void OnPropertyChanged(string propertyName)
+ {
+ OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
+ }
+ #endregion
+
+ #region Private Fields
+
+ // The private PropertyChanged event
+ private PropertyChangedEventHandler _propertyChanged;
+
+ private ExtendedPropertyCollection _extendedProperties;
+ private bool _heightChangedForCompatabity = false;
+
+ ///
+ /// Statics
+ ///
+ internal static readonly float StylusPrecision = 1000.0f;
+ internal static readonly double DefaultWidth = 2.0031496062992127;
+ internal static readonly double DefaultHeight = 2.0031496062992127;
+
+
+ #endregion
+
+ ///
+ /// Mininum acceptable stylus tip height, corresponds to 0.001 in V1
+ ///
+ /// corresponds to 0.001 in V1 (0.001 / (2540/96))
+ public static readonly double MinHeight = 0.00003779527559055120;
+
+ ///
+ /// Minimum acceptable stylus tip width
+ ///
+ /// corresponds to 0.001 in V1 (0.001 / (2540/96))
+ public static readonly double MinWidth = 0.00003779527559055120;
+
+ ///
+ /// Maximum acceptable stylus tip height.
+ ///
+ /// corresponds to 4294967 in V1 (4294967 / (2540/96))
+ public static readonly double MaxHeight = 162329.4614173230;
+
+
+ ///
+ /// Maximum acceptable stylus tip width.
+ ///
+ /// corresponds to 4294967 in V1 (4294967 / (2540/96))
+ public static readonly double MaxWidth = 162329.4614173230;
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/Events.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/Events.cs
new file mode 100644
index 0000000..7eb3ca1
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/Events.cs
@@ -0,0 +1,287 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Windows.Input;
+using WpfInk.PresentationCore.System.Windows.Input.Stylus;
+using SRID = MS.Internal.PresentationCore.SRID;
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ // ===========================================================================================
+ ///
+ /// delegate used for event handlers that are called when a stroke was was added, removed, or modified inside of a Stroke collection
+ ///
+ internal delegate void StrokeCollectionChangedEventHandler(object sender, StrokeCollectionChangedEventArgs e);
+
+ ///
+ /// Event arg used when delegate a stroke is was added, removed, or modified inside of a Stroke collection
+ ///
+ internal class StrokeCollectionChangedEventArgs : EventArgs
+ {
+ private StrokeCollection.ReadOnlyStrokeCollection _added;
+ private StrokeCollection.ReadOnlyStrokeCollection _removed;
+ private int _index = -1;
+
+ /// Constructor
+ internal StrokeCollectionChangedEventArgs(StrokeCollection added, StrokeCollection removed, int index) :
+ this(added, removed)
+ {
+ _index = index;
+ }
+
+ /// Constructor
+ public StrokeCollectionChangedEventArgs(StrokeCollection added, StrokeCollection removed)
+ {
+ if (added == null && removed == null)
+ {
+ throw new ArgumentException(SR.Get(SRID.CannotBothBeNull, "added", "removed"));
+ }
+ _added = (added == null) ? null : new StrokeCollection.ReadOnlyStrokeCollection(added);
+ _removed = (removed == null) ? null : new StrokeCollection.ReadOnlyStrokeCollection(removed);
+ }
+
+ /// Set of strokes that where added, result may be an empty collection
+ public StrokeCollection Added
+ {
+ get
+ {
+ if (_added == null)
+ {
+ _added = new StrokeCollection.ReadOnlyStrokeCollection(new StrokeCollection());
+ }
+ return _added;
+ }
+ }
+
+ /// Set of strokes that where removed, result may be an empty collection
+ public StrokeCollection Removed
+ {
+ get
+ {
+ if (_removed == null)
+ {
+ _removed = new StrokeCollection.ReadOnlyStrokeCollection(new StrokeCollection());
+ }
+ return _removed;
+ }
+ }
+
+ ///
+ /// The zero based starting index that was affected
+ ///
+ internal int Index
+ {
+ get
+ {
+ return _index;
+ }
+ }
+ }
+
+ // ===========================================================================================
+ ///
+ /// delegate used for event handlers that are called when a change to the drawing attributes associated with one or more strokes has occurred.
+ ///
+ internal delegate void PropertyDataChangedEventHandler(object sender, PropertyDataChangedEventArgs e);
+
+ ///
+ /// Event arg used a change to the drawing attributes associated with one or more strokes has occurred.
+ ///
+ internal class PropertyDataChangedEventArgs : EventArgs
+ {
+ private Guid _propertyGuid;
+ private object _newValue;
+ private object _previousValue;
+
+ /// Constructor
+ public PropertyDataChangedEventArgs(Guid propertyGuid,
+ object newValue,
+ object previousValue)
+ {
+ if (newValue == null && previousValue == null)
+ {
+ throw new ArgumentException(SR.Get(SRID.CannotBothBeNull, "newValue", "previousValue"));
+ }
+
+ _propertyGuid = propertyGuid;
+ _newValue = newValue;
+ _previousValue = previousValue;
+ }
+
+ ///
+ /// Gets the property guid that represents the DrawingAttribute that changed
+ ///
+ public Guid PropertyGuid
+ {
+ get { return _propertyGuid; }
+ }
+
+ ///
+ /// Gets the new value of the DrawingAttribute
+ ///
+ public object NewValue
+ {
+ get { return _newValue; }
+ }
+
+ ///
+ /// Gets the previous value of the DrawingAttribute
+ ///
+ public object PreviousValue
+ {
+ get { return _previousValue; }
+ }
+ }
+
+
+
+ // ===========================================================================================
+ ///
+ /// delegate used for event handlers that are called when the Custom attributes associated with an object have changed.
+ ///
+ internal delegate void ExtendedPropertiesChangedEventHandler(object sender, ExtendedPropertiesChangedEventArgs e);
+
+ ///
+ /// Event Arg used when the Custom attributes associated with an object have changed.
+ ///
+ internal class ExtendedPropertiesChangedEventArgs : EventArgs
+ {
+ private ExtendedProperty _oldProperty;
+ private ExtendedProperty _newProperty;
+
+ /// Constructor
+ internal ExtendedPropertiesChangedEventArgs(ExtendedProperty oldProperty,
+ ExtendedProperty newProperty)
+ {
+ if (oldProperty == null && newProperty == null)
+ {
+ throw new ArgumentNullException("oldProperty");
+ }
+ _oldProperty = oldProperty;
+ _newProperty = newProperty;
+ }
+
+ ///
+ /// The value of the previous property. If the Changed event was caused
+ /// by an ExtendedProperty being added, this value is null
+ ///
+ internal ExtendedProperty OldProperty
+ {
+ get { return _oldProperty; }
+ }
+
+ ///
+ /// The value of the new property. If the Changed event was caused by
+ /// an ExtendedProperty being removed, this value is null
+ ///
+ internal ExtendedProperty NewProperty
+ {
+ get { return _newProperty; }
+ }
+ }
+
+ ///
+ /// The delegate to use for the DefaultDrawingAttributesReplaced event
+ ///
+ internal delegate void DrawingAttributesReplacedEventHandler(object sender, DrawingAttributesReplacedEventArgs e);
+
+ ///
+ /// DrawingAttributesReplacedEventArgs
+ ///
+ internal class DrawingAttributesReplacedEventArgs : EventArgs
+ {
+ ///
+ /// DrawingAttributesReplacedEventArgs
+ ///
+ ///
+ /// This must be public so InkCanvas can instance it
+ ///
+ public DrawingAttributesReplacedEventArgs(DrawingAttributes newDrawingAttributes, DrawingAttributes previousDrawingAttributes)
+ {
+ if (newDrawingAttributes == null)
+ {
+ throw new ArgumentNullException("newDrawingAttributes");
+ }
+ if (previousDrawingAttributes == null)
+ {
+ throw new ArgumentNullException("previousDrawingAttributes");
+ }
+ _newDrawingAttributes = newDrawingAttributes;
+ _previousDrawingAttributes = previousDrawingAttributes;
+ }
+
+ ///
+ /// [TBS]
+ ///
+ public DrawingAttributes NewDrawingAttributes
+ {
+ get { return _newDrawingAttributes; }
+ }
+
+ ///
+ /// [TBS]
+ ///
+ public DrawingAttributes PreviousDrawingAttributes
+ {
+ get { return _previousDrawingAttributes; }
+ }
+
+ private DrawingAttributes _newDrawingAttributes;
+ private DrawingAttributes _previousDrawingAttributes;
+ }
+
+ ///
+ /// The delegate to use for the StylusPointsReplaced event
+ ///
+ internal delegate void StylusPointsReplacedEventHandler(object sender, StylusPointsReplacedEventArgs e);
+
+ ///
+ /// StylusPointsReplacedEventArgs
+ ///
+ internal class StylusPointsReplacedEventArgs : EventArgs
+ {
+ ///
+ /// StylusPointsReplacedEventArgs
+ ///
+ ///
+ /// This must be public so InkCanvas can instance it
+ ///
+ public StylusPointsReplacedEventArgs(StylusPointCollection newStylusPoints, StylusPointCollection previousStylusPoints)
+ {
+ if (newStylusPoints == null)
+ {
+ throw new ArgumentNullException("newStylusPoints");
+ }
+ if (previousStylusPoints == null)
+ {
+ throw new ArgumentNullException("previousStylusPoints");
+ }
+ _newStylusPoints = newStylusPoints;
+ _previousStylusPoints = previousStylusPoints;
+ }
+
+ ///
+ /// [TBS]
+ ///
+ public StylusPointCollection NewStylusPoints
+ {
+ get { return _newStylusPoints; }
+ }
+
+ ///
+ /// [TBS]
+ ///
+ public StylusPointCollection PreviousStylusPoints
+ {
+ get { return _previousStylusPoints; }
+ }
+
+ private StylusPointCollection _newStylusPoints;
+ private StylusPointCollection _previousStylusPoints;
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/Stroke.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/Stroke.cs
new file mode 100644
index 0000000..b32d572
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/Stroke.cs
@@ -0,0 +1,1126 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+
+using MS.Internal;
+using MS.Internal.Ink;
+using MS.Internal.Ink.InkSerializedFormat;
+
+using WpfInk.PresentationCore.System.Windows.Input.Stylus;
+using WpfInk.WindowsBase.System.Windows.Media;
+
+using SRID = MS.Internal.PresentationCore.SRID;
+
+// Primary root namespace for TabletPC/Ink/Handwriting/Recognition in .NET
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ ///
+ /// A Stroke object is the fundamental unit of ink data storage.
+ ///
+ internal partial class Stroke
+ {
+ /// Create a stroke from a StylusPointCollection
+ ///
+ ///
+ /// StylusPointCollection that makes up the stroke
+ /// drawingAttributes
+ public Stroke(StylusPointCollection stylusPoints, DrawingAttributes drawingAttributes)
+ : this(stylusPoints, drawingAttributes, null)
+ {
+ }
+
+ /// Create a stroke from a StylusPointCollection
+ ///
+ ///
+ /// StylusPointCollection that makes up the stroke
+ /// drawingAttributes
+ /// extendedProperties
+ internal Stroke(StylusPointCollection stylusPoints, DrawingAttributes drawingAttributes, ExtendedPropertyCollection extendedProperties)
+ {
+ if (stylusPoints == null)
+ {
+ throw new ArgumentNullException("stylusPoints");
+ }
+ if (stylusPoints.Count == 0)
+ {
+ throw new ArgumentException(SR.Get(SRID.InvalidStylusPointCollectionZeroCount), "stylusPoints");
+ }
+ if (drawingAttributes == null)
+ {
+ throw new ArgumentNullException("drawingAttributes");
+ }
+
+ _drawingAttributes = drawingAttributes;
+ _stylusPoints = stylusPoints;
+ _extendedProperties = extendedProperties;
+
+ Initialize();
+ }
+
+ ///
+ /// Internal helper to set up listeners, called by ctor and by Clone
+ ///
+ private void Initialize()
+ {
+ _drawingAttributes.AttributeChanged += new PropertyDataChangedEventHandler(DrawingAttributes_Changed);
+ _stylusPoints.Changed += new EventHandler(StylusPoints_Changed);
+ // 修复构建
+ //_stylusPoints.CountGoingToZero += new CancelEventHandler(StylusPoints_CountGoingToZero);
+ }
+
+ ///// Returns a new stroke that has a deep copy.
+ ///// Deep copied data includes points, point description, drawing attributes, and transform
+ ///// Deep copy of current stroke
+ //public virtual Stroke Clone()
+ //{
+ // //
+ // // use MemberwiseClone, which will instance the most derived type
+ // // We use this instead of Activator.CreateInstance because it does not
+ // // require ReflectionPermission. One thing to note, all references
+ // // are shared, including event delegates, so we need to set those to null
+ // //
+ // Stroke clone = (Stroke) this.MemberwiseClone();
+
+ // //
+ // // null the delegates in the cloned strokes
+ // //
+ // clone.DrawingAttributesChanged = null;
+ // clone.DrawingAttributesReplaced = null;
+ // clone.StylusPointsReplaced = null;
+ // clone.StylusPointsChanged = null;
+ // clone.PropertyDataChanged = null;
+ // clone.Invalidated = null;
+ // clone._propertyChanged = null;
+
+ // //Clone is also called from Stroke.Copy internally for point
+ // //erase. In that case, we don't want to clone the StylusPoints
+ // //because they will be replaced after we call
+ // if (_cloneStylusPoints)
+ // {
+ // //clone._stylusPoints = _stylusPoints.Clone();
+ // throw new NotImplementedException();
+ // }
+ // clone._drawingAttributes = _drawingAttributes.Clone();
+ // if (_extendedProperties != null)
+ // {
+ // clone._extendedProperties = _extendedProperties.Clone();
+ // }
+ // //set up listeners
+ // clone.Initialize();
+
+ // //
+ // // copy state
+ // //
+ // //Debug.Assert(_cachedGeometry == null || _cachedGeometry.IsFrozen);
+ // //we don't need to cache if this is frozen
+ // //if (null != _cachedGeometry)
+ // //{
+ // // clone._cachedGeometry = _cachedGeometry.Clone();
+ // //}
+ // //don't need to clone these, they are value types
+ // //and are copied by MemberwiseClone
+ // //_isSelected
+ // //_drawAsHollow
+ // //_cachedBounds
+
+ // //this need to be reset
+ // clone._cloneStylusPoints = true;
+
+ // return clone;
+ //}
+
+ ///
+ /// Returns a Bezier smoothed version of the StylusPoints
+ ///
+ ///
+ public StylusPointCollection GetBezierStylusPoints()
+ {
+ // Since we can't compute Bezier for single point stroke, we should return.
+ if (_stylusPoints.Count < 2)
+ {
+ return _stylusPoints;
+ }
+
+ // Construct the Bezier approximation
+ Bezier bezier = new Bezier();
+ if (!bezier.ConstructBezierState(_stylusPoints,
+ DrawingAttributes.FittingError))
+ {
+ //construction failed, return a clone of the original points
+ return _stylusPoints.Clone();
+ }
+
+ double tolerance = 0.5;
+ StylusShape stylusShape = this.DrawingAttributes.StylusShape;
+ if (null != stylusShape)
+ {
+ Rect shapeBoundingBox = stylusShape.BoundingBox;
+ double min = Math.Min(shapeBoundingBox.Width, shapeBoundingBox.Height);
+ tolerance = Math.Log10(min + min);
+ tolerance *= (StrokeCollectionSerializer.AvalonToHimetricMultiplier / 2);
+ if (tolerance < 0.5)
+ {
+ //don't allow tolerance to drop below .5 or we
+ //can wind up with an huge amount of bezier points
+ tolerance = 0.5;
+ }
+ }
+
+ List bezierPoints = bezier.Flatten(tolerance);
+ return GetInterpolatedStylusPoints(bezierPoints);
+ }
+
+ ///
+ /// Interpolate packet / pressure data from _stylusPoints
+ ///
+ private StylusPointCollection GetInterpolatedStylusPoints(List bezierPoints)
+ {
+ Debug.Assert(bezierPoints != null && bezierPoints.Count > 0);
+
+ //new points need the same description
+ StylusPointCollection bezierStylusPoints =
+ new StylusPointCollection(bezierPoints.Count);
+
+ //
+ // add the first point
+ //
+ AddInterpolatedBezierPoint(bezierStylusPoints,
+ bezierPoints[0],
+ _stylusPoints[0].PressureFactor);
+
+ if (bezierPoints.Count == 1)
+ {
+ return bezierStylusPoints;
+ }
+
+ //
+ // this is a little tricky... Bezier points are not equidistant, so we have to
+ // use the length between the points instead of the indexes to interpolate pressure
+ //
+ // Bezier points: P0 ------------------------------ P1 ---------- P2 --------- P3
+ // Stylus points: P0 -------- P1 ------------ P2 ------------- P3 ---------- P4
+ //
+ // Or in terms of lengths...
+ // Bezier lengths: L1 ------------------------------
+ // L2 ---------------------------------------------
+ // L3 ---------------------------------------------------------
+ //
+ // Stylus lengths L1 --------
+ // L2 ------------------------
+ // L3 -----------------------------------------
+ // L4 --------------------------------------------------------
+ //
+ //
+ //
+ double bezierLength = 0.0;
+ double prevUnbezierLength = 0.0;
+ double unbezierLength = GetDistanceBetweenPoints((Point) _stylusPoints[0], (Point) _stylusPoints[1]);
+
+ int stylusPointsIndex = 1;
+ int stylusPointsCount = _stylusPoints.Count;
+ //skip the first and last point
+ for (int x = 1; x < bezierPoints.Count - 1; x++)
+ {
+ bezierLength += GetDistanceBetweenPoints(bezierPoints[x - 1], bezierPoints[x]);
+ while (stylusPointsCount > stylusPointsIndex)
+ {
+ if (bezierLength >= prevUnbezierLength &&
+ bezierLength < unbezierLength)
+ {
+ Debug.Assert(stylusPointsCount > stylusPointsIndex);
+
+ StylusPoint prevStylusPoint = _stylusPoints[stylusPointsIndex - 1];
+ float percentFromPrev =
+ ((float) bezierLength - (float) prevUnbezierLength) /
+ ((float) unbezierLength - (float) prevUnbezierLength);
+ float pressureAtPrev = prevStylusPoint.PressureFactor;
+ float pressureDelta = _stylusPoints[stylusPointsIndex].PressureFactor - pressureAtPrev;
+ float interopolatedPressure = (percentFromPrev * pressureDelta) + pressureAtPrev;
+
+ AddInterpolatedBezierPoint(bezierStylusPoints,
+ bezierPoints[x],
+ interopolatedPressure);
+ break;
+ }
+ else
+ {
+ Debug.Assert(bezierLength >= prevUnbezierLength);
+ //
+ // move our unbezier lengths forward...
+ //
+ stylusPointsIndex++;
+ if (stylusPointsCount > stylusPointsIndex)
+ {
+ prevUnbezierLength = unbezierLength;
+ unbezierLength +=
+ GetDistanceBetweenPoints((Point) _stylusPoints[stylusPointsIndex - 1],
+ (Point) _stylusPoints[stylusPointsIndex]);
+ } //else we'll break
+ }
+ }
+ }
+
+ //
+ // add the last point
+ //
+ AddInterpolatedBezierPoint(bezierStylusPoints,
+ bezierPoints[bezierPoints.Count - 1],
+ _stylusPoints[stylusPointsCount - 1].PressureFactor);
+
+ return bezierStylusPoints;
+ }
+
+ ///
+ /// Private helper used to get the length between two points
+ ///
+ private double GetDistanceBetweenPoints(Point p1, Point p2)
+ {
+ Vector spine = p2 - p1;
+ return Math.Sqrt(spine.LengthSquared);
+ }
+
+ ///
+ /// Private helper for adding a StylusPoint to the BezierStylusPoints
+ ///
+ private void AddInterpolatedBezierPoint(StylusPointCollection bezierStylusPoints,
+ Point bezierPoint,
+ float pressure)
+ {
+ double xVal = bezierPoint.X > StylusPoint.MaxXY ?
+ StylusPoint.MaxXY :
+ (bezierPoint.X < StylusPoint.MinXY ? StylusPoint.MinXY : bezierPoint.X);
+
+ double yVal = bezierPoint.Y > StylusPoint.MaxXY ?
+ StylusPoint.MaxXY :
+ (bezierPoint.Y < StylusPoint.MinXY ? StylusPoint.MinXY : bezierPoint.Y);
+
+
+ StylusPoint newBezierPoint =
+ new StylusPoint(xVal, yVal, pressure);
+
+
+ bezierStylusPoints.Add(newBezierPoint);
+ }
+
+ ///
+ /// Allows addition of objects to the EPC
+ ///
+ ///
+ ///
+ public void AddPropertyData(Guid propertyDataId, object propertyData)
+ {
+ DrawingAttributes.ValidateStylusTipTransform(propertyDataId, propertyData);
+
+ object oldValue = null;
+ if (ContainsPropertyData(propertyDataId))
+ {
+ oldValue = GetPropertyData(propertyDataId);
+ this.ExtendedProperties[propertyDataId] = propertyData;
+ }
+ else
+ {
+ this.ExtendedProperties.Add(propertyDataId, propertyData);
+ }
+
+ // fire notification
+ OnPropertyDataChanged(new PropertyDataChangedEventArgs(propertyDataId, propertyData, oldValue));
+ }
+
+
+ ///
+ /// Allows removal of objects from the EPC
+ ///
+ ///
+ public void RemovePropertyData(Guid propertyDataId)
+ {
+ object propertyData = GetPropertyData(propertyDataId);
+ this.ExtendedProperties.Remove(propertyDataId);
+ // fire notification
+ OnPropertyDataChanged(new PropertyDataChangedEventArgs(propertyDataId, null, propertyData));
+ }
+
+ ///
+ /// Allows retrieval of objects from the EPC
+ ///
+ ///
+ public object GetPropertyData(Guid propertyDataId)
+ {
+ return this.ExtendedProperties[propertyDataId];
+ }
+
+ ///
+ /// Allows retrieval of a Array of guids that are contained in the EPC
+ ///
+ public Guid[] GetPropertyDataIds()
+ {
+ return this.ExtendedProperties.GetGuidArray();
+ }
+
+ ///
+ /// Allows the checking of objects in the EPC
+ ///
+ ///
+ public bool ContainsPropertyData(Guid propertyDataId)
+ {
+ return this.ExtendedProperties.Contains(propertyDataId);
+ }
+
+
+ ///
+ /// Allows an application to configure the rendering state
+ /// associated with this stroke (e.g. outline pen, brush, color,
+ /// stylus tip, etc.)
+ ///
+ ///
+ /// If the stroke has been deleted, this will return null for 'get'.
+ /// If the stroke has been deleted, the 'set' will no-op.
+ ///
+ /// The drawing attributes associated with the current stroke.
+ public DrawingAttributes DrawingAttributes
+ {
+ get
+ {
+ return _drawingAttributes;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ _drawingAttributes.AttributeChanged -= new PropertyDataChangedEventHandler(DrawingAttributes_Changed);
+
+ DrawingAttributesReplacedEventArgs e =
+ new DrawingAttributesReplacedEventArgs(value, _drawingAttributes);
+
+ DrawingAttributes previousDa = _drawingAttributes;
+ _drawingAttributes = value;
+
+
+ // If the drawing attributes change involves Width, Height, StylusTipTransform, IgnorePressure, or FitToCurve,
+ // we need to force a recaculation of the cached path geometry right after the
+ // DrawingAttributes changed, beforet the events are raised.
+ if (false == DrawingAttributes.GeometricallyEqual(previousDa, _drawingAttributes))
+ {
+ //_cachedGeometry = null;
+ // Set the cached bounds to empty, which will force a re-calculation of the _cachedBounds upon next GetBounds call.
+ _cachedBounds = Rect.Empty;
+ }
+
+ _drawingAttributes.AttributeChanged += new PropertyDataChangedEventHandler(DrawingAttributes_Changed);
+ OnDrawingAttributesReplaced(e);
+ OnInvalidated(EventArgs.Empty);
+ OnPropertyChanged(DrawingAttributesName);
+ }
+ }
+
+ ///
+ /// StylusPoints
+ ///
+ public StylusPointCollection StylusPoints
+ {
+ get
+ {
+ return _stylusPoints;
+ }
+ set
+ {
+ if (null == value)
+ {
+ throw new ArgumentNullException("value");
+ }
+ if (value.Count == 0)
+ {
+ //we don't allow this
+ throw new ArgumentException(SR.Get(SRID.InvalidStylusPointCollectionZeroCount));
+ }
+
+ // Force a recaculation of the cached path geometry
+ //_cachedGeometry = null;
+
+ // Set the cached bounds to empty, which will force a re-calculation of the _cachedBounds upon next GetBounds call.
+ _cachedBounds = Rect.Empty;
+
+ StylusPointsReplacedEventArgs e =
+ new StylusPointsReplacedEventArgs(value, _stylusPoints);
+
+ _stylusPoints.Changed -= new EventHandler(StylusPoints_Changed);
+ // 修复构建
+ //_stylusPoints.CountGoingToZero -= new CancelEventHandler(StylusPoints_CountGoingToZero);
+
+ _stylusPoints = value;
+
+ _stylusPoints.Changed += new EventHandler(StylusPoints_Changed);
+ // 修复构建
+ //_stylusPoints.CountGoingToZero += new CancelEventHandler(StylusPoints_CountGoingToZero);
+
+ // fire notification
+ OnStylusPointsReplaced(e);
+ OnInvalidated(EventArgs.Empty);
+ OnPropertyChanged(StylusPointsName);
+ }
+ }
+
+ /// Event that is fired when a drawing attribute is changed.
+ /// The event listener to add or remove in the listener chain
+ public event PropertyDataChangedEventHandler DrawingAttributesChanged;
+
+ ///
+ /// Event that is fired when the DrawingAttributes have been replaced
+ ///
+ public event DrawingAttributesReplacedEventHandler DrawingAttributesReplaced;
+
+ ///
+ /// Notifies listeners whenever the StylusPoints have been replaced
+ ///
+ public event StylusPointsReplacedEventHandler StylusPointsReplaced;
+
+ ///
+ /// Notifies listeners whenever the StylusPoints have been changed
+ ///
+ public event EventHandler StylusPointsChanged;
+
+ ///
+ /// Notifies listeners whenever a change occurs in the propertyData
+ ///
+ /// PropertyDataChangedEventHandler
+ public event PropertyDataChangedEventHandler PropertyDataChanged;
+
+
+ ///
+ /// Stroke would raise this event for PacketsChanged, DrawingAttributeChanged, or DrawingAttributeReplaced.
+ /// Renderer would simply listen to this. Stroke developer can raise this event by calling OnInvalidated when
+ /// he wants the renderer to repaint.
+ ///
+ public event EventHandler Invalidated;
+
+ ///
+ /// INotifyPropertyChanged.PropertyChanged event, explicitly implemented
+ ///
+ event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
+ {
+ add { _propertyChanged += value; }
+ remove { _propertyChanged -= value; }
+ }
+
+ ///
+ /// Method called on derived classes whenever a drawing attribute
+ /// is changed and event listeners must be notified.
+ ///
+ /// Information on the drawing attributes that changed
+ /// Derived classes should call this method (their base class)
+ /// to ensure that event listeners are notified
+ protected virtual void OnDrawingAttributesChanged(PropertyDataChangedEventArgs e)
+ {
+ if (null == e)
+ {
+ throw new ArgumentNullException("e", SR.Get(SRID.EventArgIsNull));
+ }
+
+ if (DrawingAttributesChanged != null)
+ {
+ DrawingAttributesChanged(this, e);
+ }
+ }
+
+ ///
+ /// Protected virtual version for developers deriving from InkCanvas.
+ /// This method is what actually throws the event.
+ ///
+ /// DrawingAttributesReplacedEventArgs to raise the event with
+ protected virtual void OnDrawingAttributesReplaced(DrawingAttributesReplacedEventArgs e)
+ {
+ if (e == null)
+ {
+ throw new ArgumentNullException("e");
+ }
+ if (null != this.DrawingAttributesReplaced)
+ {
+ DrawingAttributesReplaced(this, e);
+ }
+ }
+
+ ///
+ /// Method called on derived classes whenever the StylusPoints are replaced
+ ///
+ /// EventArgs
+ protected virtual void OnStylusPointsReplaced(StylusPointsReplacedEventArgs e)
+ {
+ if (null == e)
+ {
+ throw new ArgumentNullException("e", SR.Get(SRID.EventArgIsNull));
+ }
+
+ if (StylusPointsReplaced != null)
+ StylusPointsReplaced(this, e);
+ }
+
+ ///
+ /// Method called on derived classes whenever the StylusPoints are changed
+ ///
+ /// EventArgs
+ protected virtual void OnStylusPointsChanged(EventArgs e)
+ {
+ if (null == e)
+ {
+ throw new ArgumentNullException("e", SR.Get(SRID.EventArgIsNull));
+ }
+
+ if (StylusPointsChanged != null)
+ StylusPointsChanged(this, e);
+ }
+
+ ///
+ /// Method called on derived classes whenever a change occurs in
+ /// the PropertyData.
+ ///
+ /// Derived classes should call this method (their base class)
+ /// to ensure that event listeners are notified
+ protected virtual void OnPropertyDataChanged(PropertyDataChangedEventArgs e)
+ {
+ if (null == e)
+ {
+ throw new ArgumentNullException("e", SR.Get(SRID.EventArgIsNull));
+ }
+
+ if (PropertyDataChanged != null)
+ {
+ PropertyDataChanged(this, e);
+ }
+ }
+
+
+ ///
+ /// Method called on derived classes whenever a stroke needs repaint. Developers who
+ /// subclass Stroke and need a repaint could raise Invalidated through this protected virtual
+ ///
+ protected virtual void OnInvalidated(EventArgs e)
+ {
+ if (null == e)
+ {
+ throw new ArgumentNullException("e", SR.Get(SRID.EventArgIsNull));
+ }
+
+ if (Invalidated != null)
+ {
+ Invalidated(this, e);
+ }
+ }
+
+ ///
+ /// Method called when a property change occurs to the Stroke
+ ///
+ /// The EventArgs specifying the name of the changed property.
+ /// To follow the guidelines, this method should take a PropertyChangedEventArgs
+ /// instance, but every other INotifyPropertyChanged implementation follows this pattern.
+ protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
+ {
+ if (_propertyChanged != null)
+ {
+ _propertyChanged(this, e);
+ }
+ }
+
+
+ ///
+ /// ExtendedProperties
+ ///
+ internal ExtendedPropertyCollection ExtendedProperties
+ {
+ get
+ {
+ if (_extendedProperties == null)
+ {
+ _extendedProperties = new ExtendedPropertyCollection();
+ }
+
+ return _extendedProperties;
+ }
+ }
+
+
+// ///
+// /// Clip
+// ///
+// /// Fragment markers for clipping
+// private StrokeCollection Clip(StrokeFIndices[] cutAt)
+// {
+// Debug.Assert(cutAt != null);
+// Debug.Assert(cutAt.Length != 0);
+
+//#if DEBUG
+// //
+// // Assert there are no overlaps between multiple StrokeFIndices
+// //
+// AssertSortedNoOverlap(cutAt);
+//#endif
+
+// StrokeCollection leftovers = new StrokeCollection();
+// if (cutAt.Length == 0)
+// {
+// return leftovers;
+// }
+
+// if ((cutAt.Length == 1) && cutAt[0].IsFull)
+// {
+// leftovers.Add(this.Clone()); //clip and erase always return clones
+// return leftovers;
+// }
+
+
+// StylusPointCollection sourceStylusPoints = this.StylusPoints;
+// if (this.DrawingAttributes.FitToCurve)
+// {
+// sourceStylusPoints = this.GetBezierStylusPoints();
+// }
+
+// //
+// // Assert the findices are NOT out of range with the packets
+// //
+// Debug.Assert(false == ((!DoubleUtil.AreClose(cutAt[cutAt.Length - 1].EndFIndex, StrokeFIndices.AfterLast)) &&
+// Math.Ceiling(cutAt[cutAt.Length - 1].EndFIndex) > sourceStylusPoints.Count - 1));
+
+// for (int i = 0; i < cutAt.Length; i++)
+// {
+// StrokeFIndices fragment = cutAt[i];
+// if (DoubleUtil.GreaterThanOrClose(fragment.BeginFIndex, fragment.EndFIndex))
+// {
+// // ISSUE-2004/06/26-vsmirnov - temporary workaround for bugs
+// // in point erasing: drop invalid fragments
+// Debug.Assert(DoubleUtil.LessThan(fragment.BeginFIndex, fragment.EndFIndex));
+// continue;
+// }
+
+// Stroke stroke = Copy(sourceStylusPoints, fragment.BeginFIndex, fragment.EndFIndex);
+
+// // Add the stroke to the output collection
+// leftovers.Add(stroke);
+// }
+
+// return leftovers;
+// }
+
+// ///
+// ///
+// ///
+// /// Fragment markers for clipping
+// /// Survived fragments of current Stroke as a StrokeCollection
+// private StrokeCollection Erase(StrokeFIndices[] cutAt)
+// {
+// Debug.Assert(cutAt != null);
+// Debug.Assert(cutAt.Length != 0);
+
+//#if DEBUG
+// //
+// // Assert there are no overlaps between multiple StrokeFIndices
+// //
+// AssertSortedNoOverlap(cutAt);
+//#endif
+
+// StrokeCollection leftovers = new StrokeCollection();
+// // Return an empty collection if the entire stroke it to erase
+// if ((cutAt.Length == 0) || ((cutAt.Length == 1) && cutAt[0].IsFull))
+// {
+// return leftovers;
+// }
+
+// StylusPointCollection sourceStylusPoints = this.StylusPoints;
+// if (this.DrawingAttributes.FitToCurve)
+// {
+// sourceStylusPoints = this.GetBezierStylusPoints();
+// }
+
+// //
+// // Assert the findices are NOT out of range with the packets
+// //
+// Debug.Assert(false == ((!DoubleUtil.AreClose(cutAt[cutAt.Length - 1].EndFIndex, StrokeFIndices.AfterLast)) &&
+// Math.Ceiling(cutAt[cutAt.Length - 1].EndFIndex) > sourceStylusPoints.Count - 1));
+
+
+// int i = 0;
+// double beginFIndex = StrokeFIndices.BeforeFirst;
+// if (cutAt[0].BeginFIndex == StrokeFIndices.BeforeFirst)
+// {
+// beginFIndex = cutAt[0].EndFIndex;
+// i++;
+// }
+// for (; i < cutAt.Length; i++)
+// {
+// StrokeFIndices fragment = cutAt[i];
+// if (DoubleUtil.GreaterThanOrClose(beginFIndex, fragment.BeginFIndex))
+// {
+// // ISSUE-2004/06/26-vsmirnov - temporary workaround for bugs
+// // in point erasing: drop invalid fragments
+// Debug.Assert(DoubleUtil.LessThan(beginFIndex, fragment.BeginFIndex));
+// continue;
+// }
+
+
+// Stroke stroke = Copy(sourceStylusPoints, beginFIndex, fragment.BeginFIndex);
+// // Add the stroke to the output collection
+// leftovers.Add(stroke);
+
+// beginFIndex = fragment.EndFIndex;
+// }
+
+// if (beginFIndex != StrokeFIndices.AfterLast)
+// {
+// Stroke stroke = Copy(sourceStylusPoints, beginFIndex, StrokeFIndices.AfterLast);
+
+// // Add the stroke to the output collection
+// leftovers.Add(stroke);
+// }
+
+// return leftovers;
+// }
+
+
+ /////
+ ///// Creates a new stroke from a subset of the points
+ /////
+ //private Stroke Copy(StylusPointCollection sourceStylusPoints, double beginFIndex, double endFIndex)
+ //{
+ // Debug.Assert(sourceStylusPoints != null);
+ // //
+ // // get the floor and ceiling to copy from, we'll adjust the ends below
+ // //
+ // int beginIndex =
+ // (DoubleUtil.AreClose(StrokeFIndices.BeforeFirst, beginFIndex))
+ // ? 0 : (int) Math.Floor(beginFIndex);
+
+ // int endIndex =
+ // (DoubleUtil.AreClose(StrokeFIndices.AfterLast, endFIndex))
+ // ? (sourceStylusPoints.Count - 1) : (int) Math.Ceiling(endFIndex);
+
+ // int pointCount = endIndex - beginIndex + 1;
+ // Debug.Assert(pointCount >= 1);
+
+ // StylusPointCollection stylusPoints =
+ // new StylusPointCollection(pointCount);
+
+ // //
+ // // copy the data from the floor of beginIndex to the ceiling
+ // //
+ // for (int i = 0; i < pointCount; i++)
+ // {
+ // Debug.Assert(sourceStylusPoints.Count > i + beginIndex);
+ // StylusPoint stylusPoint = sourceStylusPoints[i + beginIndex];
+ // stylusPoints.Add(stylusPoint);
+ // }
+ // Debug.Assert(stylusPoints.Count == pointCount);
+
+ // //
+ // // at this point, the stroke has been reduced to one with n number of points
+ // // so we need to adjust the fIndices based on the new point data
+ // //
+ // // for example, in a stroke with 4 points:
+ // // 0, 1, 2, 3
+ // //
+ // // if the fIndexes passed 1.1 and 2.7
+ // // at this point beginIndex is 1 and endIndex is 3
+ // //
+ // // now that we've copied the stroke points 1, 2 and 3, we need to
+ // // adjust beginFIndex to .1 and endFIndex to 1.7
+ // //
+ // if (!DoubleUtil.AreClose(beginFIndex, StrokeFIndices.BeforeFirst))
+ // {
+ // beginFIndex = beginFIndex - beginIndex;
+ // }
+ // if (!DoubleUtil.AreClose(endFIndex, StrokeFIndices.AfterLast))
+ // {
+ // endFIndex = endFIndex - beginIndex;
+ // }
+
+ // if (stylusPoints.Count > 1)
+ // {
+ // Point begPoint = (Point) stylusPoints[0];
+ // Point endPoint = (Point) stylusPoints[stylusPoints.Count - 1];
+
+ // // Adjust the last point to fragment.EndFIndex.
+ // if ((!DoubleUtil.AreClose(endFIndex, StrokeFIndices.AfterLast)) && !DoubleUtil.AreClose(endIndex, endFIndex))
+ // {
+ // //
+ // // for 1.7, we need to get .3, because that is the distance
+ // // we need to back up between the third point and the second
+ // //
+ // // so this would be .3 = 2 - 1.7
+ // double ceiling = Math.Ceiling(endFIndex);
+ // double fraction = ceiling - endFIndex;
+
+ // endPoint = GetIntermediatePoint(stylusPoints[stylusPoints.Count - 1],
+ // stylusPoints[stylusPoints.Count - 2],
+ // fraction);
+ // }
+
+ // // Adjust the first point to fragment.BeginFIndex.
+ // if ((!DoubleUtil.AreClose(beginFIndex, StrokeFIndices.BeforeFirst)) && !DoubleUtil.AreClose(beginIndex, beginFIndex))
+ // {
+ // begPoint = GetIntermediatePoint(stylusPoints[0],
+ // stylusPoints[1],
+ // beginFIndex);
+ // }
+
+ // //
+ // // now set the end points
+ // //
+ // StylusPoint tempEnd = stylusPoints[stylusPoints.Count - 1];
+ // tempEnd.X = endPoint.X;
+ // tempEnd.Y = endPoint.Y;
+ // stylusPoints[stylusPoints.Count - 1] = tempEnd;
+
+ // StylusPoint tempBegin = stylusPoints[0];
+ // tempBegin.X = begPoint.X;
+ // tempBegin.Y = begPoint.Y;
+ // stylusPoints[0] = tempBegin;
+ // }
+
+ // Stroke stroke = null;
+ // try
+ // {
+ // //
+ // // set a flag that tells clone not to clone the StylusPoints
+ // // we do this in a try finally so we alway reset our state
+ // // even if Clone (which is virtual) throws
+ // //
+ // _cloneStylusPoints = false;
+ // stroke = this.Clone();
+ // if (stroke.DrawingAttributes.FitToCurve)
+ // {
+ // //
+ // // we're using the beziered points for the new data,
+ // // FitToCurve needs to be false to prevent re-bezier.
+ // //
+ // stroke.DrawingAttributes.FitToCurve = false;
+ // }
+
+ // //this will reset the cachedGeometry and cachedBounds
+ // stroke.StylusPoints = stylusPoints;
+ // }
+ // finally
+ // {
+ // _cloneStylusPoints = true;
+ // }
+
+ // return stroke;
+ //}
+
+ ///
+ /// Private helper that will generate a new point between two points at an findex
+ ///
+ private Point GetIntermediatePoint(StylusPoint p1, StylusPoint p2, double findex)
+ {
+ double xDistance = p2.X - p1.X;
+ double yDistance = p2.Y - p1.Y;
+
+ double xFDistance = xDistance * findex;
+ double yFDistance = yDistance * findex;
+
+ return new Point(p1.X + xFDistance, p1.Y + yFDistance);
+ }
+
+
+#if DEBUG
+ ///
+ /// Helper method used to validate that the strokefindices in the array
+ /// are sorted and there are no overlaps
+ ///
+ /// fragments
+ private void AssertSortedNoOverlap(StrokeFIndices[] fragments)
+ {
+ if (fragments.Length == 0)
+ {
+ return;
+ }
+ if (fragments.Length == 1)
+ {
+ Debug.Assert(IsValidStrokeFIndices(fragments[0]));
+ return;
+ }
+ double current = StrokeFIndices.BeforeFirst;
+ for (int x = 0; x < fragments.Length; x++)
+ {
+ if (fragments[x].BeginFIndex <= current)
+ {
+ //
+ // when x == 0, we're just starting, any value is valid
+ //
+ Debug.Assert(x == 0);
+ }
+ current = fragments[x].BeginFIndex;
+ Debug.Assert(IsValidStrokeFIndices(fragments[x]) && fragments[x].EndFIndex > current);
+ current = fragments[x].EndFIndex;
+ }
+ }
+
+ private bool IsValidStrokeFIndices(StrokeFIndices findex)
+ {
+ return (!double.IsNaN(findex.BeginFIndex) && !double.IsNaN(findex.EndFIndex) && findex.BeginFIndex < findex.EndFIndex);
+ }
+
+#endif
+
+
+ ///
+ /// Method called whenever the Stroke's drawing attributes are changed.
+ /// This method will trigger an event for any listeners interested in
+ /// drawing attributes.
+ ///
+ /// The Drawing Attributes object that was changed
+ /// More data about the change that occurred
+ private void DrawingAttributes_Changed(object sender, PropertyDataChangedEventArgs e)
+ {
+ // set Geometry flag to be dirty if the DA change will cause change in geometry
+ if (DrawingAttributes.IsGeometricalDaGuid(e.PropertyGuid) == true)
+ {
+ //_cachedGeometry = null;
+ // Set the cached bounds to empty, which will force a re-calculation of the _cachedBounds upon next GetBounds call.
+ _cachedBounds = Rect.Empty;
+ }
+
+ OnDrawingAttributesChanged(e);
+ if (!_delayRaiseInvalidated)
+ {
+ //when Stroke.Transform(Matrix, bool) is called, we don't raise invalidated from
+ //here, but rather from the Stroke.Transform method.
+ OnInvalidated(EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Method called whenever the Stroke's StylusPoints are changed.
+ /// This method will trigger an event for any listeners interested in
+ /// Invalidate
+ ///
+ /// The StylusPoints object that was changed
+ /// event args
+ private void StylusPoints_Changed(object sender, EventArgs e)
+ {
+ //_cachedGeometry = null;
+ _cachedBounds = Rect.Empty;
+
+ OnStylusPointsChanged(EventArgs.Empty);
+ if (!_delayRaiseInvalidated)
+ {
+ //when Stroke.Transform(Matrix, bool) is called, we don't raise invalidated from
+ //here, but rather from the Stroke.Transform method.
+ OnInvalidated(EventArgs.Empty);
+ }
+ }
+
+ ///
+ /// Private method called when StylusPoints are going to zero
+ ///
+ /// The StylusPoints object that is about to go to zero count
+ /// event args
+ private void StylusPoints_CountGoingToZero(object sender, CancelEventArgs e)
+ {
+ e.Cancel = true;
+ //StylusPoints will raise the exception
+ }
+
+ private void OnPropertyChanged(string propertyName)
+ {
+ OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
+ }
+
+ // Custom attributes associated with this stroke
+ private ExtendedPropertyCollection _extendedProperties = null;
+
+ // Drawing attributes associated with this stroke
+ private DrawingAttributes _drawingAttributes = null;
+
+ private StylusPointCollection _stylusPoints = null;
+ }
+
+ //internal helper to determine if a matix contains invalid values
+ internal static class MatrixHelper
+ {
+ //returns true if any member is NaN
+ internal static bool ContainsNaN(Matrix matrix)
+ {
+ if (Double.IsNaN(matrix.M11) ||
+ Double.IsNaN(matrix.M12) ||
+ Double.IsNaN(matrix.M21) ||
+ Double.IsNaN(matrix.M22) ||
+ Double.IsNaN(matrix.OffsetX) ||
+ Double.IsNaN(matrix.OffsetY))
+ {
+ return true;
+ }
+ return false;
+ }
+
+ //returns true if any member is negative or positive infinity
+ internal static bool ContainsInfinity(Matrix matrix)
+ {
+ if (Double.IsInfinity(matrix.M11) ||
+ Double.IsInfinity(matrix.M12) ||
+ Double.IsInfinity(matrix.M21) ||
+ Double.IsInfinity(matrix.M22) ||
+ Double.IsInfinity(matrix.OffsetX) ||
+ Double.IsInfinity(matrix.OffsetY))
+ {
+ return true;
+ }
+ return false;
+ }
+ }
+
+ ///
+ /// Helper for dealing with IEnumerable of Points
+ ///
+ internal static class IEnumerablePointHelper
+ {
+ ///
+ /// Returns the count of an IEumerable of Points by trying to cast
+ /// to an ICollection of Points
+ ///
+ internal static int GetCount(IEnumerable ienum)
+ {
+ Debug.Assert(ienum != null);
+ ICollection icol = ienum as ICollection;
+ if (icol != null)
+ {
+ return icol.Count;
+ }
+ int count = 0;
+ foreach (Point point in ienum)
+ {
+ count++;
+ }
+ return count;
+ }
+
+ ///
+ /// Returns a Point[] for a given IEnumerable of Points.
+ ///
+ internal static Point[] GetPointArray(IEnumerable ienum)
+ {
+ Debug.Assert(ienum != null);
+ Point[] points = ienum as Point[];
+ if (points != null)
+ {
+ return points;
+ }
+
+ //
+ // fall back to creating an array
+ //
+ points = new Point[GetCount(ienum)];
+ int index = 0;
+ foreach (Point point in ienum)
+ {
+ points[index++] = point;
+ }
+ return points;
+ }
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/Stroke2.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/Stroke2.cs
new file mode 100644
index 0000000..fc1f773
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/Stroke2.cs
@@ -0,0 +1,163 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+//#define DEBUG_RENDERING_FEEDBACK
+
+using System;
+using System.ComponentModel;
+
+// Primary root namespace for TabletPC/Ink/Handwriting/Recognition in .NET
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ ///
+ /// The hit-testing API of Stroke
+ ///
+ partial class Stroke : INotifyPropertyChanged
+ {
+ #region Public APIs
+
+
+ #region Public Methods
+#if false
+
+ ///
+ /// Computes the bounds of the stroke in the default rendering context
+ ///
+ ///
+ public virtual Rect GetBounds()
+ {
+ if (_cachedBounds.IsEmpty)
+ {
+ StrokeNodeIterator iterator = StrokeNodeIterator.GetIterator(this, this.DrawingAttributes);
+ for (int i = 0; i < iterator.Count; i++)
+ {
+ StrokeNode strokeNode = iterator[i];
+ _cachedBounds.Union(strokeNode.GetBounds());
+ }
+ }
+
+ return _cachedBounds;
+ }
+
+
+ ///
+ /// Render the Stroke under the specified DrawingContext. The draw method is a
+ /// batch operationg that uses the rendering methods exposed off of DrawingContext
+ ///
+ ///
+ public void Draw(DrawingContext context)
+ {
+ if (null == context)
+ {
+ throw new System.ArgumentNullException("context");
+ }
+
+ //our code never calls this public API so we can assume that opacity
+ //has not been set up
+
+ //call our public Draw method with the strokes.DA
+ this.Draw(context, this.DrawingAttributes);
+ }
+
+
+ ///
+ /// Render the StrokeCollection under the specified DrawingContext. This draw method uses the
+ /// passing in drawing attribute to override that on the stroke.
+ ///
+ ///
+ ///
+ public void Draw(DrawingContext drawingContext, DrawingAttributes drawingAttributes)
+ {
+ if (null == drawingContext)
+ {
+ throw new System.ArgumentNullException("context");
+ }
+
+ if (null == drawingAttributes)
+ {
+ throw new System.ArgumentNullException("drawingAttributes");
+ }
+
+ // context.VerifyAccess();
+
+ //our code never calls this public API so we can assume that opacity
+ //has not been set up
+
+ if (drawingAttributes.IsHighlighter)
+ {
+ drawingContext.PushOpacity(StrokeRenderer.HighlighterOpacity);
+ try
+ {
+ this.DrawInternal(drawingContext, StrokeRenderer.GetHighlighterAttributes(this, this.DrawingAttributes), false);
+ }
+ finally
+ {
+ drawingContext.Pop();
+ }
+ }
+ else
+ {
+ this.DrawInternal(drawingContext, drawingAttributes, false);
+ }
+ }
+
+#endif
+
+
+
+
+
+
+ #endregion
+
+ #endregion
+
+
+ #region Internal APIs
+
+
+
+
+ ///
+ /// Used by Inkcanvas to draw selected stroke as hollow.
+ ///
+ internal bool IsSelected
+ {
+ get { return _isSelected; }
+ set
+ {
+ if (value != _isSelected)
+ {
+ _isSelected = value;
+
+ // Raise Invalidated event. This will cause Renderer to repaint and call back DrawCore
+ OnInvalidated(EventArgs.Empty);
+ }
+ }
+ }
+
+
+ #region Private fields
+
+
+ private bool _isSelected = false;
+ private bool _drawAsHollow = false;
+ private bool _cloneStylusPoints = true;
+ private bool _delayRaiseInvalidated = false;
+ private static readonly double HollowLineSize = 1.0f;
+ private Rect _cachedBounds = Rect.Empty;
+
+ // The private PropertyChanged event
+ private PropertyChangedEventHandler _propertyChanged;
+
+ private const string DrawingAttributesName = "DrawingAttributes";
+ private const string StylusPointsName = "StylusPoints";
+
+ #endregion
+
+ internal static readonly double PercentageTolerance = 0.0001d;
+ #endregion
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/StrokeCollection.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/StrokeCollection.cs
new file mode 100644
index 0000000..88fdb12
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/StrokeCollection.cs
@@ -0,0 +1,708 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+
+using SRID = MS.Internal.PresentationCore.SRID;
+
+// Primary root namespace for TabletPC/Ink/Handwriting/Recognition in .NET
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ ///
+ /// Collection of strokes objects which can be operated on in aggregate.
+ ///
+ internal partial class StrokeCollection
+ {
+ ///
+ /// The string used to designate the native persistence format
+ /// for ink data (e.g. used on the clipboard)
+ ///
+ public static readonly String InkSerializedFormat = "Ink Serialized Format";
+
+ /// Creates an empty stroke collection
+ public StrokeCollection()
+ {
+ }
+
+ /// Creates a StrokeCollection based on a collection of existing strokes
+ public StrokeCollection(IEnumerable strokes)
+ {
+ if (strokes == null)
+ {
+ throw new ArgumentNullException("strokes");
+ }
+
+ List items = (List) this.Items;
+
+ //unfortunately we have to check for dupes with this ctor
+ foreach (Stroke stroke in strokes)
+ {
+ if (items.Contains(stroke))
+ {
+ //clear and throw
+ items.Clear();
+ throw new ArgumentException(SR.Get(SRID.StrokeIsDuplicated), "strokes");
+ }
+ items.Add(stroke);
+ }
+ }
+
+
+// ///
+// /// Performs a deep copy of the StrokeCollection.
+// ///
+// public virtual StrokeCollection Clone()
+// {
+// StrokeCollection clone = new StrokeCollection();
+// foreach (Stroke s in this)
+// {
+// // samgeo - Presharp issue
+// // Presharp gives a warning when get methods might deref a null. It's complaining
+// // here that s could be null, but StrokeCollection never allows nulls to be added
+// // so this is not possible
+//#pragma warning disable 1634, 1691
+//#pragma warning suppress 6506
+// clone.Add(s.Clone());
+//#pragma warning restore 1634, 1691
+// }
+
+// //
+// // clone epc if we have them
+// //
+// if (_extendedProperties != null)
+// {
+// clone._extendedProperties = _extendedProperties.Clone();
+// }
+// return clone;
+// }
+
+ ///
+ /// called by base class Collection<T> when the list is being cleared;
+ /// raises a CollectionChanged event to any listeners
+ ///
+ protected override sealed void ClearItems()
+ {
+ if (this.Count > 0)
+ {
+ StrokeCollection removed = new StrokeCollection();
+ for (int x = 0; x < this.Count; x++)
+ {
+ ((List) removed.Items).Add(this[x]);
+ }
+
+ base.ClearItems();
+
+ RaiseStrokesChanged(null /*added*/, removed, -1);
+ }
+ }
+
+ ///
+ /// called by base class RemoveAt or Remove methods
+ ///
+ protected override sealed void RemoveItem(int index)
+ {
+ Stroke removedStroke = this[index];
+ base.RemoveItem(index);
+
+ StrokeCollection removed = new StrokeCollection();
+ ((List) removed.Items).Add(removedStroke);
+ RaiseStrokesChanged(null /*added*/, removed, index);
+ }
+
+ ///
+ /// called by base class Insert, Add methods
+ ///
+ protected override sealed void InsertItem(int index, Stroke stroke)
+ {
+ if (stroke == null)
+ {
+ throw new ArgumentNullException("stroke");
+ }
+ if (this.IndexOf(stroke) != -1)
+ {
+ throw new ArgumentException(SR.Get(SRID.StrokeIsDuplicated), "stroke");
+ }
+
+ base.InsertItem(index, stroke);
+
+ StrokeCollection addedStrokes = new StrokeCollection();
+ ((List) addedStrokes.Items).Add(stroke);
+ RaiseStrokesChanged(addedStrokes, null /*removed*/, index);
+ }
+
+ ///
+ /// called by base class set_Item method
+ ///
+ protected override sealed void SetItem(int index, Stroke stroke)
+ {
+ if (stroke == null)
+ {
+ throw new ArgumentNullException("stroke");
+ }
+ if (IndexOf(stroke) != -1)
+ {
+ throw new ArgumentException(SR.Get(SRID.StrokeIsDuplicated), "stroke");
+ }
+
+ Stroke removedStroke = this[index];
+ base.SetItem(index, stroke);
+
+ StrokeCollection removed = new StrokeCollection();
+ ((List) removed.Items).Add(removedStroke);
+
+ StrokeCollection added = new StrokeCollection();
+ ((List) added.Items).Add(stroke);
+ RaiseStrokesChanged(added, removed, index);
+ }
+
+ ///
+ /// Gets the index of the stroke, or -1 if it is not found
+ ///
+ /// stroke
+ ///
+ public new int IndexOf(Stroke stroke)
+ {
+ if (stroke == null)
+ {
+ //we never allow null strokes
+ return -1;
+ }
+ for (int i = 0; i < Count; i++)
+ {
+ if (object.ReferenceEquals(this[i], stroke))
+ {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ ///
+ /// Remove a set of Stroke objects to the collection
+ ///
+ /// The strokes to remove from the collection
+ /// Changes to the collection trigger a StrokesChanged event.
+ public void Remove(StrokeCollection strokes)
+ {
+ if (strokes == null)
+ {
+ throw new ArgumentNullException("strokes");
+ }
+ if (strokes.Count == 0)
+ {
+ // NOTICE-2004/06/08-WAYNEZEN:
+ // We don't throw if an empty collection is going to be removed. And there is no event either.
+ // This rule is also applied to invoking Clear() with an empty StrokeCollection.
+ return;
+ }
+
+ int[] indexes = this.GetStrokeIndexes(strokes);
+ if (indexes == null)
+ {
+ // At least one stroke doesn't exist in our collection. We throw.
+ ArgumentException ae = new ArgumentException(SR.Get(SRID.InvalidRemovedStroke), "strokes");
+ //
+ // we add a tag here so we can check for this in EraserBehavior.OnPointEraseResultChanged
+ // to determine if this method is the origin of an ArgumentException we harden against
+ //
+ ae.Data.Add("System.Windows.Ink.StrokeCollection", "");
+ throw ae;
+ }
+
+ for (int x = indexes.Length - 1; x >= 0; x--)
+ {
+ //bypass this.RemoveAt, which calls changed events
+ //and call our protected List directly
+ //remove from the back so the indexes are correct
+ ((List) this.Items).RemoveAt(indexes[x]);
+ }
+
+ RaiseStrokesChanged(null /*added*/, strokes, indexes[0]);
+ }
+
+ ///
+ /// Add a set of Stroke objects to the collection
+ ///
+ /// The strokes to add to the collection
+ /// The items are added to the collection at the end of the list.
+ /// If the item already exists in the collection, then the item is not added again.
+ public void Add(StrokeCollection strokes)
+ {
+ if (strokes == null)
+ {
+ throw new ArgumentNullException("strokes");
+ }
+ if (strokes.Count == 0)
+ {
+ // NOTICE-2004/06/08-WAYNEZEN:
+ // We don't throw if an empty collection is going to be added. And there is no event either.
+ return;
+ }
+
+ int index = this.Count;
+
+ //validate that none of the strokes exist in the collection
+ for (int x = 0; x < strokes.Count; x++)
+ {
+ Stroke stroke = strokes[x];
+ if (this.IndexOf(stroke) != -1)
+ {
+ throw new ArgumentException(SR.Get(SRID.StrokeIsDuplicated), "strokes");
+ }
+ }
+
+ //add the strokes
+ //bypass this.AddRange, which calls changed events
+ //and call our protected List directly
+ ((List) this.Items).AddRange(strokes);
+
+ RaiseStrokesChanged(strokes, null /*removed*/, index);
+ }
+
+ ///
+ /// Replace
+ ///
+ ///
+ ///
+ public void Replace(Stroke strokeToReplace, StrokeCollection strokesToReplaceWith)
+ {
+ if (strokeToReplace == null)
+ {
+ throw new ArgumentNullException(SR.Get(SRID.EmptyScToReplace));
+ }
+
+ StrokeCollection strokesToReplace = new StrokeCollection();
+ strokesToReplace.Add(strokeToReplace);
+ this.Replace(strokesToReplace, strokesToReplaceWith);
+ }
+
+ ///
+ /// Replace
+ ///
+ ///
+ ///
+ public void Replace(StrokeCollection strokesToReplace, StrokeCollection strokesToReplaceWith)
+ {
+ if (strokesToReplace == null)
+ {
+ throw new ArgumentNullException(SR.Get(SRID.EmptyScToReplace));
+ }
+ if (strokesToReplaceWith == null)
+ {
+ throw new ArgumentNullException(SR.Get(SRID.EmptyScToReplaceWith));
+ }
+
+ int replaceCount = strokesToReplace.Count;
+ if (replaceCount == 0)
+ {
+ ArgumentException ae = new ArgumentException(SR.Get(SRID.EmptyScToReplace), "strokesToReplace");
+ //
+ // we add a tag here so we can check for this in EraserBehavior.OnPointEraseResultChanged
+ // to determine if this method is the origin of an ArgumentException we harden against
+ //
+ ae.Data.Add("System.Windows.Ink.StrokeCollection", "");
+ throw ae;
+ }
+
+ int[] indexes = this.GetStrokeIndexes(strokesToReplace);
+ if (indexes == null)
+ {
+ // At least one stroke doesn't exist in our collection. We throw.
+ ArgumentException ae = new ArgumentException(SR.Get(SRID.InvalidRemovedStroke), "strokesToReplace");
+ //
+ // we add a tag here so we can check for this in EraserBehavior.OnPointEraseResultChanged
+ // to determine if this method is the origin of an ArgumentException we harden against
+ //
+ ae.Data.Add("System.Windows.Ink.StrokeCollection", "");
+ throw ae;
+ }
+
+
+ //validate that none of the relplaceWith strokes exist in the collection
+ for (int x = 0; x < strokesToReplaceWith.Count; x++)
+ {
+ Stroke stroke = strokesToReplaceWith[x];
+ if (this.IndexOf(stroke) != -1)
+ {
+ throw new ArgumentException(SR.Get(SRID.StrokeIsDuplicated), "strokesToReplaceWith");
+ }
+ }
+
+ //bypass this.RemoveAt / InsertRange, which calls changed events
+ //and call our protected List directly
+ for (int x = indexes.Length - 1; x >= 0; x--)
+ {
+ //bypass this.RemoveAt, which calls changed events
+ //and call our protected List directly
+ //remove from the back so the indexes are correct
+ ((List) this.Items).RemoveAt(indexes[x]);
+ }
+
+ if (strokesToReplaceWith.Count > 0)
+ {
+ //insert at the
+ ((List) this.Items).InsertRange(indexes[0], strokesToReplaceWith);
+ }
+
+
+ RaiseStrokesChanged(strokesToReplaceWith, strokesToReplace, indexes[0]);
+ }
+
+ ///
+ /// called by StrokeCollectionSerializer during Load, bypasses Change notification
+ ///
+ internal void AddWithoutEvent(Stroke stroke)
+ {
+ Debug.Assert(stroke != null && IndexOf(stroke) == -1);
+ ((List) this.Items).Add(stroke);
+ }
+
+
+ /// Collection of extended properties on this StrokeCollection
+ internal ExtendedPropertyCollection ExtendedProperties
+ {
+ get
+ {
+ //
+ // internal getter is used by the serialization code
+ //
+ if (_extendedProperties == null)
+ {
+ _extendedProperties = new ExtendedPropertyCollection();
+ }
+
+ return _extendedProperties;
+ }
+ private set
+ {
+ //
+ // private setter used by copy
+ //
+ if (value != null)
+ {
+ _extendedProperties = value;
+ }
+ }
+ }
+
+ ///
+ /// Event that notifies listeners whenever a change occurs in the set
+ /// of stroke objects contained in the collection.
+ ///
+ /// StrokeCollectionChangedEventHandler
+ public event StrokeCollectionChangedEventHandler StrokesChanged;
+
+ ///
+ /// Event that notifies internal listeners whenever a change occurs in the set
+ /// of stroke objects contained in the collection.
+ ///
+ /// StrokeCollectionChangedEventHandler
+ internal event StrokeCollectionChangedEventHandler StrokesChangedInternal;
+
+ ///
+ /// Event that notifies listeners whenever a change occurs in the propertyData
+ ///
+ /// PropertyDataChangedEventHandler
+ public event PropertyDataChangedEventHandler PropertyDataChanged;
+
+ ///
+ /// INotifyPropertyChanged.PropertyChanged event, explicitly implemented
+ ///
+ event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
+ {
+ add { _propertyChanged += value; }
+ remove { _propertyChanged -= value; }
+ }
+
+ ///
+ /// INotifyCollectionChanged.CollectionChanged event, explicitly implemented
+ ///
+ event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
+ {
+ add { _collectionChanged += value; }
+ remove { _collectionChanged -= value; }
+ }
+
+
+ /// Method called on derived classes whenever a drawing attributes
+ /// change has occurred in the stroke references in the collection
+ /// The change information for the stroke collection
+ /// StrokesChanged will not be called when drawing attributes or
+ /// custom attributes are changed. Changes that trigger StrokesChanged
+ /// include packets or points changing, modified tranforms, and stroke objects
+ /// being added or removed from the collection.
+ /// To ensure that events fire for event listeners, derived classes
+ /// should call this method.
+ protected virtual void OnStrokesChanged(StrokeCollectionChangedEventArgs e)
+ {
+ if (null == e)
+ {
+ throw new ArgumentNullException("e", SR.Get(SRID.EventArgIsNull));
+ }
+
+ //raise our internal event first. This is used by
+ //our Renderer and IncrementalHitTester since if they can assume
+ //they are the first in the delegate chain, they can be optimized
+ //to not have to handle out of order events caused by 3rd party code
+ //getting called first
+ if (this.StrokesChangedInternal != null)
+ {
+ this.StrokesChangedInternal(this, e);
+ }
+ if (this.StrokesChanged != null)
+ {
+ this.StrokesChanged(this, e);
+ }
+ if (_collectionChanged != null)
+ {
+ //raise CollectionChanged. We support the following
+ //NotifyCollectionChangedActions
+ NotifyCollectionChangedEventArgs args = null;
+ if (this.Count == 0)
+ {
+ //Reset
+ Debug.Assert(e.Removed.Count > 0);
+ args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+ }
+ else if (e.Added.Count == 0)
+ {
+ //Remove
+ args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, e.Removed, e.Index);
+ }
+ else if (e.Removed.Count == 0)
+ {
+ //Add
+ args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, e.Added, e.Index);
+ }
+ else
+ {
+ //Replace
+ args = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, e.Added, e.Removed, e.Index);
+ }
+ _collectionChanged(this, args);
+ }
+ }
+
+
+ ///
+ /// Method called on derived classes whenever a change occurs in
+ /// the PropertyData.
+ ///
+ /// Derived classes should call this method (their base class)
+ /// to ensure that event listeners are notified
+ protected virtual void OnPropertyDataChanged(PropertyDataChangedEventArgs e)
+ {
+ if (null == e)
+ {
+ throw new ArgumentNullException("e", SR.Get(SRID.EventArgIsNull));
+ }
+
+ if (this.PropertyDataChanged != null)
+ {
+ this.PropertyDataChanged(this, e);
+ }
+ }
+
+ ///
+ /// Method called when a property change occurs to the StrokeCollection
+ ///
+ /// The EventArgs specifying the name of the changed property.
+ /// To follow the guidelines, this method should take a PropertyChangedEventArgs
+ /// instance, but every other INotifyPropertyChanged implementation follows this pattern.
+ protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
+ {
+ if (_propertyChanged != null)
+ {
+ _propertyChanged(this, e);
+ }
+ }
+
+
+ ///
+ /// Private helper that starts searching for stroke at index,
+ /// but will loop around before reporting -1. This is used for
+ /// Stroke.Remove(StrokeCollection). For example, if we're removing
+ /// strokes, chances are they are in contiguous order. If so, calling
+ /// IndexOf to validate each stroke is O(n2). If the strokes are in order
+ /// this produces closer to O(n), if they are not in order, it is no worse
+ ///
+ private int OptimisticIndexOf(int startingIndex, Stroke stroke)
+ {
+ Debug.Assert(startingIndex >= 0);
+ for (int x = startingIndex; x < this.Count; x++)
+ {
+ if (this[x] == stroke)
+ {
+ return x;
+ }
+ }
+
+ //we didn't find anything on the first pass, now search the beginning
+ for (int x = 0; x < startingIndex; x++)
+ {
+ if (this[x] == stroke)
+ {
+ return x;
+ }
+ }
+ return -1;
+ }
+
+ ///
+ /// Private helper that returns an array of indexes where the specified
+ /// strokes exist in this stroke collection. Returns null if at least one is not found.
+ ///
+ /// The indexes are sorted from smallest to largest
+ ///
+ ///
+ private int[] GetStrokeIndexes(StrokeCollection strokes)
+ {
+ //to keep from walking the StrokeCollection twice for each stroke, we will maintain an index of
+ //strokes to remove as we go
+ int[] indexes = new int[strokes.Count];
+ for (int x = 0; x < indexes.Length; x++)
+ {
+ indexes[x] = Int32.MaxValue;
+ }
+
+ int currentIndex = 0;
+ int highestIndex = -1;
+ int usedIndexCount = 0;
+ for (int x = 0; x < strokes.Count; x++)
+ {
+ currentIndex = this.OptimisticIndexOf(currentIndex, strokes[x]);
+ if (currentIndex == -1)
+ {
+ //stroke doe3sn't exist, bail out.
+ return null;
+ }
+
+ //
+ // optimize for the most common case... replace is passes strokes
+ // in contiguous order. Only do the sort if we need to
+ //
+ if (currentIndex > highestIndex)
+ {
+ //write current to the next available slot
+ indexes[usedIndexCount++] = currentIndex;
+ highestIndex = currentIndex;
+ continue;
+ }
+
+ //keep in sorted order (smallest to largest) with a simple insertion sort
+ for (int y = 0; y < indexes.Length; y++)
+ {
+ if (currentIndex < indexes[y])
+ {
+ if (indexes[y] != Int32.MaxValue)
+ {
+ //shift from the end
+ for (int i = indexes.Length - 1; i > y; i--)
+ {
+ indexes[i] = indexes[i - 1];
+ }
+ }
+ indexes[y] = currentIndex;
+ usedIndexCount++;
+
+ if (currentIndex > highestIndex)
+ {
+ highestIndex = currentIndex;
+ }
+ break;
+ }
+ }
+ }
+
+ return indexes;
+ }
+
+ // This function will invoke OnStrokesChanged method.
+ // addedStrokes - the collection which contains the added strokes during the previous op.
+ // removedStrokes - the collection which contains the removed strokes during the previous op.
+ private void RaiseStrokesChanged(StrokeCollection addedStrokes, StrokeCollection removedStrokes, int index)
+ {
+ StrokeCollectionChangedEventArgs eventArgs =
+ new StrokeCollectionChangedEventArgs(addedStrokes, removedStrokes, index);
+
+ // Invoke OnPropertyChanged
+ OnPropertyChanged(CountName);
+ OnPropertyChanged(IndexerName);
+
+ // Invoke OnStrokesChanged which will fire the StrokesChanged event AND the CollectionChanged event.
+ OnStrokesChanged(eventArgs);
+ }
+
+ private void OnPropertyChanged(string propertyName)
+ {
+ OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
+ }
+
+ // Custom 'user-defined' attributes assigned to this collection
+ // In v1, these were called Ink.ExtendedProperties
+ private ExtendedPropertyCollection _extendedProperties = null;
+
+ // The private PropertyChanged event
+ private PropertyChangedEventHandler _propertyChanged;
+
+ // private CollectionChanged event raiser
+ private NotifyCollectionChangedEventHandler _collectionChanged;
+
+ ///
+ /// Constants for the PropertyChanged event
+ ///
+ private const string IndexerName = "Item[]";
+ private const string CountName = "Count";
+
+ //
+ // Nested types...
+ //
+
+ ///
+ /// ReadOnlyStrokeCollection - for StrokeCollection.StrokesChanged event args...
+ ///
+ internal class ReadOnlyStrokeCollection : StrokeCollection, ICollection, IList
+ {
+ internal ReadOnlyStrokeCollection(StrokeCollection strokeCollection)
+ {
+ if (strokeCollection != null)
+ {
+ ((List) this.Items).AddRange(strokeCollection);
+ }
+ }
+
+ ///
+ /// Change is not allowed. We would override SetItem, InsertItem etc but
+ /// they need to be sealed on StrokeCollection to prevent dupes from being added
+ ///
+ ///
+ protected override void OnStrokesChanged(StrokeCollectionChangedEventArgs e)
+ {
+ throw new NotSupportedException(SR.Get(SRID.StrokeCollectionIsReadOnly));
+ }
+
+ ///
+ /// IsReadOnly
+ ///
+ bool IList.IsReadOnly
+ {
+ get { return true; }
+ }
+
+ ///
+ /// IsReadOnly
+ ///
+ bool ICollection.IsReadOnly
+ {
+ get { return true; }
+ }
+ }
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/StrokeCollection2.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/StrokeCollection2.cs
new file mode 100644
index 0000000..b96a9d3
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/StrokeCollection2.cs
@@ -0,0 +1,380 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Diagnostics;
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ ///
+ /// The hit-testing API of StrokeCollection.
+ ///
+ internal partial class StrokeCollection : Collection, INotifyPropertyChanged, INotifyCollectionChanged
+ {
+ #region Public APIs
+
+
+ // ISSUE-2004/12/13-XIAOTU: In M8.2, the following two tap-hit APIs return the top-hit stroke,
+ // giving preference to non-highlighter strokes. We have decided not to treat highlighter and
+ // non-highlighter differently and only return the top-hit stroke. But there are two remaining
+ // open-issues on this:
+ // 1. Do we need to make these two APIs virtual, so user can treat highlighter differently if they
+ // want to?
+ // 2. Since we are only returning the top-hit stroke, should we use Stroke as the return type?
+ //
+#if false
+ ///
+ /// Tap-hit. Hit tests all strokes within a point, and returns a StrokeCollection for these strokes.Internally does Stroke.HitTest(Point, 1pxlRectShape).
+ ///
+ /// A StrokeCollection that either empty or contains the top hit stroke
+ public StrokeCollection HitTest(Point point)
+ {
+ return PointHitTest(point, new RectangleStylusShape(1f, 1f));
+ }
+
+ ///
+ /// Tap-hit
+ ///
+ /// The central point
+ /// The diameter value of the circle
+ /// A StrokeCollection that either empty or contains the top hit stroke
+ public StrokeCollection HitTest(Point point, double diameter)
+ {
+ if (Double.IsNaN(diameter) || diameter < DrawingAttributes.MinWidth || diameter > DrawingAttributes.MaxWidth)
+ {
+ throw new ArgumentOutOfRangeException("diameter", SR.Get(SRID.InvalidDiameter));
+ }
+ return PointHitTest(point, new EllipseStylusShape(diameter, diameter));
+ }
+
+ ///
+ /// Hit-testing with lasso
+ ///
+ /// points making the lasso
+ /// the margin value to tell whether a stroke
+ /// is in or outside of the rect
+ /// collection of strokes found inside the rectangle
+ public StrokeCollection HitTest(IEnumerable lassoPoints, int percentageWithinLasso)
+ {
+ // Check the input parameters
+ if (lassoPoints == null)
+ {
+ throw new System.ArgumentNullException("lassoPoints");
+ }
+ if ((percentageWithinLasso < 0) || (percentageWithinLasso > 100))
+ {
+ throw new System.ArgumentOutOfRangeException("percentageWithinLasso");
+ }
+
+ if (IEnumerablePointHelper.GetCount(lassoPoints) < 3)
+ {
+ return new StrokeCollection();
+ }
+
+ Lasso lasso = new SingleLoopLasso();
+ lasso.AddPoints(lassoPoints);
+
+ // Enumerate through the strokes and collect those captured by the lasso.
+ StrokeCollection lassoedStrokes = new StrokeCollection();
+ foreach (Stroke stroke in this)
+ {
+ if (percentageWithinLasso == 0)
+ {
+ lassoedStrokes.Add(stroke);
+ }
+ else
+ {
+ StrokeInfo strokeInfo = null;
+ try
+ {
+ strokeInfo = new StrokeInfo(stroke);
+
+ StylusPointCollection stylusPoints = strokeInfo.StylusPoints;
+ double target = strokeInfo.TotalWeight * percentageWithinLasso / 100.0f - Stroke.PercentageTolerance;
+
+ for (int i = 0; i < stylusPoints.Count; i++)
+ {
+ if (true == lasso.Contains((Point)stylusPoints[i]))
+ {
+ target -= strokeInfo.GetPointWeight(i);
+ if (DoubleUtil.LessThanOrClose(target, 0f))
+ {
+ lassoedStrokes.Add(stroke);
+ break;
+ }
+ }
+ }
+ }
+ finally
+ {
+ if (strokeInfo != null)
+ {
+ //detach from event handlers, or else we leak.
+ strokeInfo.Detach();
+ }
+ }
+ }
+ }
+
+ // Return the resulting collection
+ return lassoedStrokes;
+ }
+
+ ///
+ /// Hit-testing with rectangle
+ ///
+ /// hitting rectangle
+ /// the percentage of the stroke that must be within
+ /// the bounds to be considered hit
+ /// collection of strokes found inside the rectangle
+ public StrokeCollection HitTest(Rect bounds, int percentageWithinBounds)
+ {
+ // Check the input parameters
+ if ((percentageWithinBounds < 0) || (percentageWithinBounds > 100))
+ {
+ throw new System.ArgumentOutOfRangeException("percentageWithinBounds");
+ }
+ if (bounds.IsEmpty)
+ {
+ return new StrokeCollection();
+ }
+
+ // Enumerate thru the strokes collect those found within the rectangle.
+ StrokeCollection hits = new StrokeCollection();
+ foreach (Stroke stroke in this)
+ {
+ // samgeo - Presharp issue
+ // Presharp gives a warning when get methods might deref a null. It's complaining
+ // here that 'stroke'' could be null, but StrokeCollection never allows nulls to be added
+ // so this is not possible
+#pragma warning disable 1634, 1691
+#pragma warning suppress 6506
+ if (true == stroke.HitTest(bounds, percentageWithinBounds))
+ {
+ hits.Add(stroke);
+ }
+#pragma warning restore 1634, 1691
+ }
+ return hits;
+ }
+
+
+ ///
+ /// Issue: what's the return value
+ ///
+ ///
+ ///
+ ///
+ public StrokeCollection HitTest(IEnumerable path, StylusShape stylusShape)
+ {
+ // Check the input parameters
+ if (stylusShape == null)
+ {
+ throw new System.ArgumentNullException("stylusShape");
+ }
+ if (path == null)
+ {
+ throw new System.ArgumentNullException("path");
+ }
+ if (IEnumerablePointHelper.GetCount(path) == 0)
+ {
+ return new StrokeCollection();
+ }
+
+ // validate input
+ ErasingStroke erasingStroke = new ErasingStroke(stylusShape, path);
+ Rect erasingBounds = erasingStroke.Bounds;
+ if (erasingBounds.IsEmpty)
+ {
+ return new StrokeCollection();
+ }
+ StrokeCollection hits = new StrokeCollection();
+ foreach (Stroke stroke in this)
+ {
+ // samgeo - Presharp issue
+ // Presharp gives a warning when get methods might deref a null. It's complaining
+ // here that 'stroke'' could be null, but StrokeCollection never allows nulls to be added
+ // so this is not possible
+#pragma warning disable 1634, 1691
+#pragma warning suppress 6506
+ if (erasingBounds.IntersectsWith(stroke.GetBounds()) &&
+ erasingStroke.HitTest(StrokeNodeIterator.GetIterator(stroke, stroke.DrawingAttributes)))
+ {
+ hits.Add(stroke);
+ }
+#pragma warning restore 1634, 1691
+ }
+
+ return hits;
+ }
+
+ ///
+ /// Clips out all ink outside a given lasso
+ ///
+ /// lasso
+ public void Clip(IEnumerable lassoPoints)
+ {
+ // Check the input parameters
+ if (lassoPoints == null)
+ {
+ throw new System.ArgumentNullException("lassoPoints");
+ }
+
+ int length = IEnumerablePointHelper.GetCount(lassoPoints);
+ if (length == 0)
+ {
+ throw new ArgumentException(SR.Get(SRID.EmptyArray));
+ }
+
+ if (length < 3)
+ {
+ //
+ // if you're clipping with a point or a line with
+ // two points, it doesn't matter where the line is or if it
+ // intersects any of the strokes, the point or line has no region
+ // so technically everything in the strokecollection
+ // should be removed
+ //
+ this.Clear(); //raises the appropriate events
+ return;
+ }
+
+ Lasso lasso = new SingleLoopLasso();
+ lasso.AddPoints(lassoPoints);
+
+ for (int i = 0; i < this.Count; i++)
+ {
+ Stroke stroke = this[i];
+ StrokeCollection clipResult = stroke.Clip(stroke.HitTest(lasso));
+ UpdateStrokeCollection(stroke, clipResult, ref i);
+ }
+ }
+
+ ///
+ /// Clips out all ink outside a given rectangle.
+ ///
+ /// rectangle to clip with
+ public void Clip(Rect bounds)
+ {
+ if (bounds.IsEmpty == false)
+ {
+ Clip(new Point[4] { bounds.TopLeft, bounds.TopRight, bounds.BottomRight, bounds.BottomLeft });
+ }
+ }
+
+#endif
+
+
+#if false
+
+ ///
+ /// Render the StrokeCollection under the specified DrawingContext.
+ ///
+ ///
+ public void Draw(DrawingContext context)
+ {
+ if (null == context)
+ {
+ throw new System.ArgumentNullException("context");
+ }
+
+ //The verification of UI context affinity is done in Stroke.Draw()
+
+ List solidStrokes = new List();
+ Dictionary> highLighters = new Dictionary>();
+
+ for (int i = 0; i < this.Count; i++)
+ {
+ Stroke stroke = this[i];
+ List strokes;
+ if (stroke.DrawingAttributes.IsHighlighter)
+ {
+ // It's very important to override the Alpha value so that Colors of the same RGB vale
+ // but different Alpha would be in the same list.
+ Color color = StrokeRenderer.GetHighlighterColor(stroke.DrawingAttributes.Color);
+ if (highLighters.TryGetValue(color, out strokes) == false)
+ {
+ strokes = new List();
+ highLighters.Add(color, strokes);
+ }
+ strokes.Add(stroke);
+ }
+ else
+ {
+ solidStrokes.Add(stroke);
+ }
+ }
+
+ foreach (List strokes in highLighters.Values)
+ {
+ context.PushOpacity(StrokeRenderer.HighlighterOpacity);
+ try
+ {
+ foreach (Stroke stroke in strokes)
+ {
+ stroke.DrawInternal(context, StrokeRenderer.GetHighlighterAttributes(stroke, stroke.DrawingAttributes),
+ false /*Don't draw selected stroke as hollow*/);
+ }
+ }
+ finally
+ {
+ context.Pop();
+ }
+ }
+
+ foreach(Stroke stroke in solidStrokes)
+ {
+ stroke.DrawInternal(context, stroke.DrawingAttributes, false/*Don't draw selected stroke as hollow*/);
+ }
+ }
+#endif
+
+ #endregion
+
+
+
+#if false
+
+ ///
+ /// Return all hit strokes that the StylusShape intersects and returns them in a StrokeCollection
+ ///
+ private StrokeCollection PointHitTest(Point point, StylusShape shape)
+ {
+ // Create the collection to return
+ StrokeCollection hits = new StrokeCollection();
+ for (int i = 0; i < this.Count; i++)
+ {
+ Stroke stroke = this[i];
+ if (stroke.HitTest(new Point[] { point }, shape))
+ {
+ hits.Add(stroke);
+ }
+ }
+
+ return hits;
+ }
+#endif
+
+ private void UpdateStrokeCollection(Stroke original, StrokeCollection toReplace, ref int index)
+ {
+ Debug.Assert(original != null && toReplace != null);
+ Debug.Assert(index >= 0 && index < this.Count);
+ if (toReplace.Count == 0)
+ {
+ Remove(original);
+ index--;
+ }
+ else if (!(toReplace.Count == 1 && toReplace[0] == original))
+ {
+ Replace(original, toReplace);
+
+ // Update the current index
+ index += toReplace.Count - 1;
+ }
+ }
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/StylusTip.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/StylusTip.cs
new file mode 100644
index 0000000..a715ac3
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Ink/StylusTip.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+
+namespace WpfInk.PresentationCore.System.Windows.Ink
+{
+ ///
+ /// StylusTip
+ ///
+ internal enum StylusTip
+ {
+ ///
+ /// Rectangle
+ ///
+ Rectangle = 0,
+
+ ///
+ /// Ellipse
+ ///
+ Ellipse
+ }
+
+ ///
+ /// Internal helper to avoid costly call to Enum.IsDefined
+ ///
+ internal static class StylusTipHelper
+ {
+ internal static bool IsDefined(StylusTip stylusTip)
+ {
+ if (stylusTip < StylusTip.Rectangle || stylusTip > StylusTip.Ellipse)
+ {
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPoint.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPoint.cs
new file mode 100644
index 0000000..02ae751
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPoint.cs
@@ -0,0 +1,402 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using WpfInk.PresentationCore.System.Windows.Ink;
+
+namespace WpfInk.PresentationCore.System.Windows.Input.Stylus
+{
+ ///
+ /// Represents a single sampling point from a stylus input device
+ ///
+ internal struct StylusPoint : IEquatable
+ {
+ internal const float DefaultPressure = 0.5f;
+
+
+ private double _x;
+ private double _y;
+ private float _pressureFactor;
+
+ #region Constructors
+ ///
+ /// StylusPoint
+ ///
+ /// x
+ /// y
+ public StylusPoint(double x, double y)
+ : this(x, y, DefaultPressure, null, null, false, false)
+ {
+ }
+
+ ///
+ /// StylusPoint
+ ///
+ /// x
+ /// y
+ /// pressureFactor
+ public StylusPoint(double x, double y, float pressureFactor)
+ : this(x, y, pressureFactor, null, null, false, true)
+ {
+ }
+
+
+ ///
+ /// StylusPoint
+ ///
+ /// x
+ /// y
+ /// pressureFactor
+ /// stylusPointDescription
+ /// additionalValues
+ public StylusPoint(double x, double y, float pressureFactor, StylusPointDescription stylusPointDescription, int[] additionalValues)
+ : this(x, y, pressureFactor, stylusPointDescription, additionalValues, true, true)
+ {
+ }
+
+ ///
+ /// internal ctor
+ ///
+ internal StylusPoint(
+ double x,
+ double y,
+ float pressureFactor,
+ StylusPointDescription stylusPointDescription,
+ int[] additionalValues,
+ bool validateAdditionalData,
+ bool validatePressureFactor)
+ {
+ if (Double.IsNaN(x))
+ {
+ throw new ArgumentOutOfRangeException(nameof(x), SR.InvalidStylusPointXYNaN);
+ }
+ if (Double.IsNaN(y))
+ {
+ throw new ArgumentOutOfRangeException(nameof(y), SR.InvalidStylusPointXYNaN);
+ }
+
+
+ //we don't validate pressure when called by StylusPointDescription.Reformat
+ if (validatePressureFactor &&
+ (pressureFactor == Single.NaN || pressureFactor < 0.0f || pressureFactor > 1.0f))
+ {
+ throw new ArgumentOutOfRangeException(nameof(pressureFactor), SR.InvalidPressureValue);
+ }
+ //
+ // only accept values between MaxXY and MinXY
+ // we don't throw when passed a value outside of that range, we just silently trunctate
+ //
+ _x = GetClampedXYValue(x);
+ _y = GetClampedXYValue(y);
+ _pressureFactor = pressureFactor;
+
+ if (validateAdditionalData)
+ {
+ //
+ // called from the public verbose ctor
+ //
+ ArgumentNullException.ThrowIfNull(stylusPointDescription);
+
+ //
+ // additionalValues can be null if PropertyCount == 3 (X, Y, P)
+ //
+ if (stylusPointDescription.PropertyCount > StylusPointDescription.RequiredCountOfProperties)
+ {
+ ArgumentNullException.ThrowIfNull(additionalValues);
+ }
+
+ }
+ }
+
+
+
+ #endregion Constructors
+
+ ///
+ /// The Maximum X or Y value supported for backwards compatibility with previous inking platforms
+ ///
+ public static readonly double MaxXY = 81164736.28346430d;
+
+ ///
+ /// The Minimum X or Y value supported for backwards compatibility with previous inking platforms
+ ///
+ public static readonly double MinXY = -81164736.32125960d;
+
+ ///
+ /// X
+ ///
+ public double X
+ {
+ get { return _x; }
+ set
+ {
+ if (Double.IsNaN(value))
+ {
+ throw new ArgumentOutOfRangeException("X", SR.InvalidStylusPointXYNaN);
+ }
+ //
+ // only accept values between MaxXY and MinXY
+ // we don't throw when passed a value outside of that range, we just silently trunctate
+ //
+ _x = GetClampedXYValue(value);
+ }
+ }
+
+ ///
+ /// Y
+ ///
+ public double Y
+ {
+ get { return _y; }
+ set
+ {
+ if (Double.IsNaN(value))
+ {
+ throw new ArgumentOutOfRangeException("Y", SR.InvalidStylusPointXYNaN);
+ }
+ //
+ // only accept values between MaxXY and MinXY
+ // we don't throw when passed a value outside of that range, we just silently trunctate
+ //
+ _y = GetClampedXYValue(value);
+ }
+ }
+
+ ///
+ /// PressureFactor. A value between 0.0 (no pressure) and 1.0 (max pressure)
+ ///
+ public float PressureFactor
+ {
+ get
+ {
+ //
+ // note that pressure can be stored a > 1 or < 0.
+ // we need to clamp if this is the case
+ //
+ if (_pressureFactor > 1.0f)
+ {
+ return 1.0f;
+ }
+ if (_pressureFactor < 0.0f)
+ {
+ return 0.0f;
+ }
+ return _pressureFactor;
+ }
+ set
+ {
+ if (value < 0.0f || value > 1.0f)
+ {
+ throw new ArgumentOutOfRangeException("PressureFactor", SR.InvalidPressureValue);
+ }
+ _pressureFactor = value;
+ }
+ }
+
+
+ ///
+ /// Provides read access to all stylus properties
+ ///
+ /// The StylusPointPropertyIds of the property to retrieve
+ public int GetPropertyValue(StylusPointProperty stylusPointProperty)
+ {
+ ArgumentNullException.ThrowIfNull(stylusPointProperty);
+ if (stylusPointProperty.Id == StylusPointPropertyIds.X)
+ {
+ return (int) _x;
+ }
+ else if (stylusPointProperty.Id == StylusPointPropertyIds.Y)
+ {
+ return (int) _y;
+ }
+ else if (stylusPointProperty.Id == StylusPointPropertyIds.NormalPressure)
+ {
+ //StylusPointPropertyInfo info =
+ // this.Description.GetPropertyInfo(StylusPointProperties.NormalPressure);
+
+ //int max = info.Maximum;
+ return (int) _pressureFactor * 1024;
+ }
+ else
+ {
+ throw new ArgumentException(SR.InvalidStylusPointProperty, nameof(stylusPointProperty));
+ }
+ }
+
+ ///
+ /// Explicit cast converter between StylusPoint and Point
+ ///
+ /// stylusPoint
+ public static explicit operator Point(StylusPoint stylusPoint)
+ {
+ return new Point(stylusPoint.X, stylusPoint.Y);
+ }
+
+ ///
+ /// Allows languages that don't support operator overloading
+ /// to convert to a point
+ ///
+ public Point ToPoint()
+ {
+ return new Point(this.X, this.Y);
+ }
+
+
+ ///
+ /// Compares two StylusPoint instances for exact equality.
+ /// Note that double values can acquire error when operated upon, such that
+ /// an exact comparison between two values which are logically equal may fail.
+ /// Furthermore, using this equality operator, Double.NaN is not equal to itself.
+ /// Descriptions must match for equality to succeed and additional values must match
+ ///
+ ///
+ /// bool - true if the two Stylus instances are exactly equal, false otherwise
+ ///
+ /// The first StylusPoint to compare
+ /// The second StylusPoint to compare
+ public static bool operator ==(StylusPoint stylusPoint1, StylusPoint stylusPoint2)
+ {
+ return StylusPoint.Equals(stylusPoint1, stylusPoint2);
+ }
+
+ ///
+ /// Compares two StylusPoint instances for exact inequality.
+ /// Note that double values can acquire error when operated upon, such that
+ /// an exact comparison between two values which are logically equal may fail.
+ /// Furthermore, using this equality operator, Double.NaN is not equal to itself.
+ ///
+ ///
+ /// bool - true if the two Stylus instances are exactly inequal, false otherwise
+ ///
+ /// The first StylusPoint to compare
+ /// The second StylusPoint to compare
+ public static bool operator !=(StylusPoint stylusPoint1, StylusPoint stylusPoint2)
+ {
+ return !StylusPoint.Equals(stylusPoint1, stylusPoint2);
+ }
+
+ ///
+ /// Compares two StylusPoint instances for exact equality.
+ /// Note that double values can acquire error when operated upon, such that
+ /// an exact comparison between two values which are logically equal may fail.
+ /// Furthermore, using this equality operator, Double.NaN is not equal to itself.
+ /// Descriptions must match for equality to succeed and additional values must match
+ ///
+ ///
+ /// bool - true if the two Stylus instances are exactly equal, false otherwise
+ ///
+ /// The first StylusPoint to compare
+ /// The second StylusPoint to compare
+ public static bool Equals(StylusPoint stylusPoint1, StylusPoint stylusPoint2)
+ {
+ //
+ // do the cheap comparison first
+ //
+ bool membersEqual =
+ stylusPoint1._x == stylusPoint2._x &&
+ stylusPoint1._y == stylusPoint2._y &&
+ stylusPoint1._pressureFactor == stylusPoint2._pressureFactor;
+
+ if (!membersEqual)
+ {
+ return false;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Compares two StylusPoint instances for exact equality.
+ /// Note that double values can acquire error when operated upon, such that
+ /// an exact comparison between two values which are logically equal may fail.
+ /// Furthermore, using this equality operator, Double.NaN is not equal to itself.
+ /// Descriptions must match for equality to succeed and additional values must match
+ ///
+ ///
+ /// bool - true if the object is an instance of StylusPoint and if it's equal to "this".
+ ///
+ /// The object to compare to "this"
+ public override bool Equals(object o)
+ {
+ if ((null == o) || !(o is StylusPoint))
+ {
+ return false;
+ }
+
+ StylusPoint value = (StylusPoint) o;
+ return StylusPoint.Equals(this, value);
+ }
+
+ ///
+ /// Equals - compares this StylusPoint with the passed in object. In this equality
+ /// Double.NaN is equal to itself, unlike in numeric equality.
+ /// Note that double values can acquire error when operated upon, such that
+ /// an exact comparison between two values which
+ /// are logically equal may fail.
+ ///
+ ///
+ /// bool - true if "value" is equal to "this".
+ ///
+ /// The StylusPoint to compare to "this"
+ public bool Equals(StylusPoint value)
+ {
+ return StylusPoint.Equals(this, value);
+ }
+ ///
+ /// Returns the HashCode for this StylusPoint
+ ///
+ ///
+ /// int - the HashCode for this StylusPoint
+ ///
+ public override int GetHashCode()
+ {
+ int hash =
+ _x.GetHashCode() ^
+ _y.GetHashCode() ^
+ _pressureFactor.GetHashCode();
+
+ return hash;
+ }
+
+
+ ///
+ /// Internal helper used by SPC.Reformat to preserve the pressureFactor
+ ///
+ internal float GetUntruncatedPressureFactor()
+ {
+ return _pressureFactor;
+ }
+
+ ///
+ /// Internal helper to determine if a stroke has default pressure
+ /// This is used by ISF serialization to not serialize pressure
+ ///
+ internal bool HasDefaultPressure
+ {
+ get
+ {
+ return (_pressureFactor == DefaultPressure);
+ }
+ }
+
+ ///
+ /// Private helper that returns a double clamped to MaxXY or MinXY
+ /// We only accept values in this range to support ISF serialization
+ ///
+ private static double GetClampedXYValue(double xyValue)
+ {
+ if (xyValue > MaxXY)
+ {
+ return MaxXY;
+ }
+ if (xyValue < MinXY)
+ {
+ return MinXY;
+ }
+
+ return xyValue;
+ }
+ }
+}
diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointCollection.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointCollection.cs
new file mode 100644
index 0000000..71340c1
--- /dev/null
+++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointCollection.cs
@@ -0,0 +1,478 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Windows.Input;
+
+namespace WpfInk.PresentationCore.System.Windows.Input.Stylus
+{
+ ///
+ /// StylusPointCollection
+ ///
+ internal class StylusPointCollection : Collection
+ {
+ ///
+ /// Changed event, anytime the data in this collection changes, this event is raised
+ ///
+ public event EventHandler? Changed;
+
+ ///
+ /// Internal only changed event used by Stroke to prevent zero count strokes
+ ///
+ internal event CancelEventHandler? CountGoingToZero;
+
+ ///
+ /// StylusPointCollection
+ ///
+ public StylusPointCollection()
+ {
+ }
+
+ ///
+ /// StylusPointCollection
+ ///
+ /// initialCapacity
+ public StylusPointCollection(int initialCapacity)
+ : this()
+ {
+ if (initialCapacity < 0)
+ {
+ throw new ArgumentException(SR.InvalidStylusPointConstructionZeroLengthCollection, nameof(initialCapacity));
+ }
+ ((List) this.Items).Capacity = initialCapacity;
+ }
+
+
+ ///
+ /// StylusPointCollection
+ ///
+ /// stylusPointDescription
+ /// initialCapacity
+ public StylusPointCollection(StylusPointDescription stylusPointDescription, int initialCapacity)
+ : this()
+ {
+ if (initialCapacity < 0)
+ {
+ throw new ArgumentException(SR.InvalidStylusPointConstructionZeroLengthCollection, nameof(initialCapacity));
+ }
+ ((List) this.Items).Capacity = initialCapacity;
+ }
+
+
+ ///
+ /// StylusPointCollection
+ ///
+ /// points
+ public StylusPointCollection(IEnumerable points)
+ : this()
+ {
+ ArgumentNullException.ThrowIfNull(points);
+
+ List stylusPoints = new List();
+ foreach (Point point in points)
+ {
+ //this can throw (since point.X or Y can be beyond our range)
+ //don't add to our internal collection until after we instance
+ //all of the styluspoints and we know the ranges are valid
+ stylusPoints.Add(new StylusPoint(point.X, point.Y));
+ }
+
+ if (stylusPoints.Count == 0)
+ {
+ throw new ArgumentException(SR.InvalidStylusPointConstructionZeroLengthCollection, nameof(points));
+ }
+
+ ((List) this.Items).Capacity = stylusPoints.Count;
+ ((List) this.Items).AddRange(stylusPoints);
+ }
+
+#if false
+
+ ///