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 + + /// + /// Internal ctor called by input with a raw int[] + /// + /// stylusPointDescription + /// rawPacketData + /// tabletToView + /// tabletToView + internal StylusPointCollection(StylusPointDescription stylusPointDescription, int[] rawPacketData, GeneralTransform tabletToView, Matrix tabletToViewMatrix) + { + ArgumentNullException.ThrowIfNull(stylusPointDescription); + _stylusPointDescription = stylusPointDescription; + + int lengthPerPoint = stylusPointDescription.GetInputArrayLengthPerPoint(); + int logicalPointCount = rawPacketData.Length / lengthPerPoint; + Debug.Assert(0 == rawPacketData.Length % lengthPerPoint, "Invalid assumption about packet length, there shouldn't be any remainder"); + + // + // set our capacity and validate + // + ((List) this.Items).Capacity = logicalPointCount; + for (int count = 0, i = 0; count < logicalPointCount; count++, i += lengthPerPoint) + { + //first, determine the x, y values by xf-ing them + Point p = new Point(rawPacketData[i], rawPacketData[i + 1]); + if (tabletToView != null) + { + tabletToView.TryTransform(p, out p); + } + else + { + p = tabletToViewMatrix.Transform(p); + } + + int startIndex = 2; + bool containsTruePressure = stylusPointDescription.ContainsTruePressure; + if (containsTruePressure) + { + //don't copy pressure in the int[] for extra data + startIndex++; + } + + int[] data = null; + int dataLength = lengthPerPoint - startIndex; + if (dataLength > 0) + { + //copy the rest of the data + var rawArrayStartIndex = i + startIndex; + data = rawPacketData.AsSpan(rawArrayStartIndex, dataLength).ToArray(); + } + + StylusPoint newPoint = new StylusPoint(p.X, p.Y, StylusPoint.DefaultPressure, _stylusPointDescription, data, false, false); + if (containsTruePressure) + { + //use the algorithm to set pressure in StylusPoint + int pressure = rawPacketData[i + 2]; + newPoint.SetPropertyValue(StylusPointProperties.NormalPressure, pressure); + } + + //this does not go through our protected virtuals + ((List) this.Items).Add(newPoint); + } + } +#endif + + /// + /// Adds the StylusPoints in the StylusPointCollection to this StylusPointCollection + /// + /// stylusPoints + public void Add(StylusPointCollection stylusPoints) + { + //note that we don't raise an exception if stylusPoints.Count == 0 + ArgumentNullException.ThrowIfNull(stylusPoints); + + + // cache count outside of the loop, so if this SPC is ever passed + // we don't loop forever + int count = stylusPoints.Count; + for (int x = 0; x < count; x++) + { + StylusPoint stylusPoint = stylusPoints[x]; + //this does not go through our protected virtuals + ((List) this.Items).Add(stylusPoint); + } + + if (stylusPoints.Count > 0) + { + OnChanged(EventArgs.Empty); + } + } + + /// + /// called by base class Collection<T> when the list is being cleared; + /// raises a CollectionChanged event to any listeners + /// + protected sealed override void ClearItems() + { + if (CanGoToZero()) + { + base.ClearItems(); + OnChanged(EventArgs.Empty); + } + else + { + throw new InvalidOperationException(SR.InvalidStylusPointCollectionZeroCount); + } + } + + /// + /// called by base class Collection<T> when an item is removed from list; + /// raises a CollectionChanged event to any listeners + /// + protected sealed override void RemoveItem(int index) + { + if (this.Count > 1 || CanGoToZero()) + { + base.RemoveItem(index); + OnChanged(EventArgs.Empty); + } + else + { + throw new InvalidOperationException(SR.InvalidStylusPointCollectionZeroCount); + } + } + + /// + /// called by base class Collection<T> when an item is added to list; + /// raises a CollectionChanged event to any listeners + /// + protected sealed override void InsertItem(int index, StylusPoint stylusPoint) + { + base.InsertItem(index, stylusPoint); + + OnChanged(EventArgs.Empty); + } + + /// + /// called by base class Collection<T> when an item is set in list; + /// raises a CollectionChanged event to any listeners + /// + protected sealed override void SetItem(int index, StylusPoint stylusPoint) + { + base.SetItem(index, stylusPoint); + + OnChanged(EventArgs.Empty); + } + + /// + /// Clone + /// + public StylusPointCollection Clone() + { + return this.Clone(/*System.Windows.Media.Transform.Identity,*/ /*this.Description,*/ this.Count); + } + + /// + /// Explicit cast converter between StylusPointCollection and Point[] + /// + /// stylusPoints + public static explicit operator Point[](StylusPointCollection stylusPoints) + { + if (stylusPoints == null) + { + return null; + } + + Point[] points = new Point[stylusPoints.Count]; + for (int i = 0; i < stylusPoints.Count; i++) + { + points[i] = new Point(stylusPoints[i].X, stylusPoints[i].Y); + } + return points; + } + +#if false + /// + /// Clone and truncate + /// + /// The maximum count of points to clone (used by GestureRecognizer) + /// + internal StylusPointCollection Clone(int count) + { + ArgumentOutOfRangeException.ThrowIfGreaterThan(count, this.Count); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(count); + + return this.Clone(System.Windows.Media.Transform.Identity, this.Description, count); + } + + /// + /// Clone with a transform, used by input + /// + internal StylusPointCollection Clone(GeneralTransform transform, StylusPointDescription descriptionToUse) + { + return this.Clone(transform, descriptionToUse, this.Count); + } +#endif + + + /// + /// Private clone implementation + /// + private StylusPointCollection Clone(/*GeneralTransform transform,*/ /*StylusPointDescription descriptionToUse,*/ int count) + { + Debug.Assert(count <= this.Count); + // + // We don't need to copy our _stylusPointDescription because it is immutable + // and we don't need to copy our StylusPoints, because they are structs. + // + StylusPointCollection newCollection = + new StylusPointCollection(count); + + bool isIdentity = //(transform is Transform) ? ((Transform) transform).IsIdentity : false; + true; + for (int x = 0; x < count; x++) + { + if (isIdentity) + { + ((List) newCollection.Items).Add(this[x]); + } + else + { +#if false + Point point = new Point(); + StylusPoint stylusPoint = this[x]; + point.X = stylusPoint.X; + point.Y = stylusPoint.Y; + transform.TryTransform(point, out point); + stylusPoint.X = point.X; + stylusPoint.Y = point.Y; + ((List) newCollection.Items).Add(stylusPoint); +#endif + throw new NotImplementedException(); + } + } + return newCollection; + } + + /// + /// Protected virtual for raising changed notification + /// + /// + protected virtual void OnChanged(EventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + if (this.Changed != null) + { + this.Changed(this, e); + } + } + +#if false + /// + /// Transform the StylusPoints in this collection by the specified transform + /// + /// transform + internal void Transform(GeneralTransform transform) + { + Point point = new Point(); + for (int i = 0; i < this.Count; i++) + { + StylusPoint stylusPoint = this[i]; + point.X = stylusPoint.X; + point.Y = stylusPoint.Y; + transform.TryTransform(point, out point); + stylusPoint.X = point.X; + stylusPoint.Y = point.Y; + + //this does not go through our protected virtuals + ((List) this.Items)[i] = stylusPoint; + } + + if (this.Count > 0) + { + this.OnChanged(EventArgs.Empty); + } + } + /// + /// Reformat + /// + /// subsetToReformatTo + public StylusPointCollection Reformat(StylusPointDescription subsetToReformatTo) + { + return Reformat(subsetToReformatTo, System.Windows.Media.Transform.Identity); + } + + /// + /// Helper that transforms and scales in one go + /// + internal StylusPointCollection Reformat(StylusPointDescription subsetToReformatTo, GeneralTransform transform) + { + if (!subsetToReformatTo.IsSubsetOf(this.Description)) + { + throw new ArgumentException(SR.InvalidStylusPointDescriptionSubset, nameof(subsetToReformatTo)); + } + + StylusPointDescription subsetToReformatToWithCurrentMetrics = + StylusPointDescription.GetCommonDescription(subsetToReformatTo, + this.Description); //preserve metrics from this spd + + if (StylusPointDescription.AreCompatible(this.Description, subsetToReformatToWithCurrentMetrics) && + (transform is Transform) && ((Transform) transform).IsIdentity) + { + //subsetToReformatTo might have different x, y, p metrics + return this.Clone(transform, subsetToReformatToWithCurrentMetrics); + } + + // + // we really need to reformat this... + // + StylusPointCollection newCollection = new StylusPointCollection(subsetToReformatToWithCurrentMetrics, this.Count); + int additionalDataCount = subsetToReformatToWithCurrentMetrics.GetExpectedAdditionalDataCount(); + + ReadOnlyCollection properties + = subsetToReformatToWithCurrentMetrics.GetStylusPointProperties(); + bool isIdentity = (transform is Transform) ? ((Transform) transform).IsIdentity : false; + + for (int i = 0; i < this.Count; i++) + { + StylusPoint stylusPoint = this[i]; + + double xCoord = stylusPoint.X; + double yCoord = stylusPoint.Y; + float pressure = stylusPoint.GetUntruncatedPressureFactor(); + + if (!isIdentity) + { + Point p = new Point(xCoord, yCoord); + transform.TryTransform(p, out p); + xCoord = p.X; + yCoord = p.Y; + } + + int[] newData = null; + if (additionalDataCount > 0) + { + //don't init, we'll do that below + newData = new int[additionalDataCount]; + } + + StylusPoint newStylusPoint = + new StylusPoint(xCoord, yCoord, pressure, subsetToReformatToWithCurrentMetrics, newData, false, false); + + //start at 3, skipping x, y, pressure + for (int x = StylusPointDescription.RequiredCountOfProperties/*3*/; x < properties.Count; x++) + { + int value = stylusPoint.GetPropertyValue(properties[x]); + newStylusPoint.SetPropertyValue(properties[x], value, copyBeforeWrite: false); + } + //bypass validation + ((List) newCollection.Items).Add(newStylusPoint); + } + return newCollection; + } +#endif + + + /// + /// Private helper use to consult with any listening strokes if it is safe to go to zero count + /// + /// + private bool CanGoToZero() + { + if (null == this.CountGoingToZero) + { + // + // no one is listening + // + return true; + } + + CancelEventArgs e = new CancelEventArgs + { + Cancel = false + }; + + // + // call the listeners + // + this.CountGoingToZero(this, e); + Debug.Assert(e.Cancel, "This event should always be cancelled"); + + return !e.Cancel; + } + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointDescription.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointDescription.cs new file mode 100644 index 0000000..20a7ffc --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointDescription.cs @@ -0,0 +1,420 @@ +// 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.Diagnostics; +using System.Windows.Input; +using WpfInk.PresentationCore.System.Windows.Ink; + +namespace WpfInk.PresentationCore.System.Windows.Input.Stylus +{ + /// + /// StylusPointDescription describes the properties that a StylusPoint supports. + /// + internal class StylusPointDescription + { + /// + /// Internal statics for our magic numbers + /// + internal const int RequiredCountOfProperties = 3; + internal const int RequiredXIndex = 0; + internal const int RequiredYIndex = 1; + internal const int RequiredPressureIndex = 2; + internal const int MaximumButtonCount = 31; + + private int _buttonCount = 0; + private int _originalPressureIndex = RequiredPressureIndex; + private StylusPointPropertyInfo[] _stylusPointPropertyInfos; + + /// + /// StylusPointDescription + /// + public StylusPointDescription() + { + //implement the default packet description + _stylusPointPropertyInfos = + new StylusPointPropertyInfo[] + { + StylusPointPropertyInfoDefaults.X, + StylusPointPropertyInfoDefaults.Y, + StylusPointPropertyInfoDefaults.NormalPressure + }; + } + + /// + /// StylusPointDescription + /// + public StylusPointDescription(IEnumerable stylusPointPropertyInfos) + { + ArgumentNullException.ThrowIfNull(stylusPointPropertyInfos); + List infos = + new List(stylusPointPropertyInfos); + + if (infos.Count < RequiredCountOfProperties || + infos[RequiredXIndex].Id != StylusPointPropertyIds.X || + infos[RequiredYIndex].Id != StylusPointPropertyIds.Y || + infos[RequiredPressureIndex].Id != StylusPointPropertyIds.NormalPressure) + { + throw new ArgumentException(SR.InvalidStylusPointDescription, nameof(stylusPointPropertyInfos)); + } + + // + // look for duplicates, validate that buttons are last + // + List seenIds = new List(); + seenIds.Add(StylusPointPropertyIds.X); + seenIds.Add(StylusPointPropertyIds.Y); + seenIds.Add(StylusPointPropertyIds.NormalPressure); + + int buttonCount = 0; + for (int x = RequiredCountOfProperties; x < infos.Count; x++) + { + if (seenIds.Contains(infos[x].Id)) + { + throw new ArgumentException(SR.InvalidStylusPointDescriptionDuplicatesFound, nameof(stylusPointPropertyInfos)); + } + if (infos[x].IsButton) + { + buttonCount++; + } + else + { + //this is not a button, make sure we haven't seen one before + if (buttonCount > 0) + { + throw new ArgumentException(SR.InvalidStylusPointDescriptionButtonsMustBeLast, nameof(stylusPointPropertyInfos)); + } + } + seenIds.Add(infos[x].Id); + } + if (buttonCount > MaximumButtonCount) + { + throw new ArgumentException(SR.InvalidStylusPointDescriptionTooManyButtons, nameof(stylusPointPropertyInfos)); + } + + _buttonCount = buttonCount; + _stylusPointPropertyInfos = infos.ToArray(); + } + + /// + /// StylusPointDescription + /// + /// stylusPointPropertyInfos + /// originalPressureIndex - does the digitizer really support pressure? If so, the index this was at + internal StylusPointDescription(IEnumerable stylusPointPropertyInfos, int originalPressureIndex) + : this(stylusPointPropertyInfos) + { + _originalPressureIndex = originalPressureIndex; + } + + /// + /// HasProperty + /// + /// stylusPointProperty + public bool HasProperty(StylusPointProperty stylusPointProperty) + { + ArgumentNullException.ThrowIfNull(stylusPointProperty); + + int index = IndexOf(stylusPointProperty.Id); + if (-1 == index) + { + return false; + } + return true; + } + + /// + /// The count of properties this StylusPointDescription contains + /// + public int PropertyCount + { + get { return _stylusPointPropertyInfos.Length; } + } + + /// + /// GetProperty + /// + /// stylusPointProperty + public StylusPointPropertyInfo GetPropertyInfo(StylusPointProperty stylusPointProperty) + { + ArgumentNullException.ThrowIfNull(stylusPointProperty); + return GetPropertyInfo(stylusPointProperty.Id); + } + + /// + /// GetPropertyInfo + /// + /// guid + internal StylusPointPropertyInfo GetPropertyInfo(Guid guid) + { + int index = IndexOf(guid); + if (-1 == index) + { + //we didn't find it + throw new ArgumentException("stylusPointProperty"); + } + return _stylusPointPropertyInfos[index]; + } + + /// + /// Returns the index of the given StylusPointProperty by ID, or -1 if none is found + /// + internal int GetPropertyIndex(Guid guid) + { + return IndexOf(guid); + } + + /// + /// GetStylusPointProperties + /// + public ReadOnlyCollection GetStylusPointProperties() + { + return new ReadOnlyCollection(_stylusPointPropertyInfos); + } + + /// + /// GetStylusPointPropertyIdss + /// + internal Guid[] GetStylusPointPropertyIds() + { + Guid[] ret = new Guid[_stylusPointPropertyInfos.Length]; + for (int x = 0; x < ret.Length; x++) + { + ret[x] = _stylusPointPropertyInfos[x].Id; + } + return ret; + } + + /// + /// Internal helper for determining how many ints in a raw int array + /// correspond to one point we get from the input system + /// + internal int GetInputArrayLengthPerPoint() + { + int buttonLength = _buttonCount > 0 ? 1 : 0; + int propertyLength = (_stylusPointPropertyInfos.Length - _buttonCount) + buttonLength; + if (!this.ContainsTruePressure) + { + propertyLength--; + } + return propertyLength; + } + + /// + /// Internal helper for determining how many members a StylusPoint's + /// internal int[] should be for additional data + /// + internal int GetExpectedAdditionalDataCount() + { + int buttonLength = _buttonCount > 0 ? 1 : 0; + int expectedLength = ((_stylusPointPropertyInfos.Length - _buttonCount) + buttonLength) - 3 /*x, y, p*/; + return expectedLength; + } + + /// + /// Internal helper for determining how many ints in a raw int array + /// correspond to one point when saving to himetric + /// + /// + internal int GetOutputArrayLengthPerPoint() + { + int length = GetInputArrayLengthPerPoint(); + if (!this.ContainsTruePressure) + { + length++; + } + return length; + } + + /// + /// Internal helper for determining how many buttons are present + /// + internal int ButtonCount + { + get + { + return _buttonCount; + } + } + + /// + /// Internal helper for determining what bit position the button is at + /// + internal int GetButtonBitPosition(StylusPointProperty buttonProperty) + { + if (!buttonProperty.IsButton) + { + throw new InvalidOperationException(); + } + int buttonIndex = 0; + for (int x = _stylusPointPropertyInfos.Length - _buttonCount; //start of the buttons + x < _stylusPointPropertyInfos.Length; x++) + { + if (_stylusPointPropertyInfos[x].Id == buttonProperty.Id) + { + return buttonIndex; + } + if (_stylusPointPropertyInfos[x].IsButton) + { + // we're in the buttons, but this isn't the right one, + // bump the button index and keep looking + buttonIndex++; + } + } + return -1; + } + + /// + /// ContainsTruePressure - true if this StylusPointDescription was instanced + /// by a TabletDevice or by ISF serialization that contains NormalPressure + /// + internal bool ContainsTruePressure + { + get { return (_originalPressureIndex != -1); } + } + + /// + /// Internal helper to determine the original pressure index + /// + internal int OriginalPressureIndex + { + get { return _originalPressureIndex; } + } + + /// + /// Returns true if the two StylusPointDescriptions have the same StylusPointProperties. Metrics are ignored. + /// + /// stylusPointDescription1 + /// stylusPointDescription2 + public static bool AreCompatible(StylusPointDescription stylusPointDescription1, StylusPointDescription stylusPointDescription2) + { + if (stylusPointDescription1 == null || stylusPointDescription2 == null) + { + throw new ArgumentNullException("stylusPointDescription"); + } + + // if a StylusPointDescription is not null, then _stylusPointPropertyInfos is not null. + // + // ignore X, Y, Pressure - they are guaranteed to be the first3 members + // + Debug.Assert(stylusPointDescription1._stylusPointPropertyInfos.Length >= RequiredCountOfProperties && + stylusPointDescription1._stylusPointPropertyInfos[0].Id == StylusPointPropertyIds.X && + stylusPointDescription1._stylusPointPropertyInfos[1].Id == StylusPointPropertyIds.Y && + stylusPointDescription1._stylusPointPropertyInfos[2].Id == StylusPointPropertyIds.NormalPressure); + + Debug.Assert(stylusPointDescription2._stylusPointPropertyInfos.Length >= RequiredCountOfProperties && + stylusPointDescription2._stylusPointPropertyInfos[0].Id == StylusPointPropertyIds.X && + stylusPointDescription2._stylusPointPropertyInfos[1].Id == StylusPointPropertyIds.Y && + stylusPointDescription2._stylusPointPropertyInfos[2].Id == StylusPointPropertyIds.NormalPressure); + + if (stylusPointDescription1._stylusPointPropertyInfos.Length != stylusPointDescription2._stylusPointPropertyInfos.Length) + { + return false; + } + for (int x = RequiredCountOfProperties; x < stylusPointDescription1._stylusPointPropertyInfos.Length; x++) + { + if (!StylusPointPropertyInfo.AreCompatible(stylusPointDescription1._stylusPointPropertyInfos[x], stylusPointDescription2._stylusPointPropertyInfos[x])) + { + return false; + } + } + + return true; + } + + /// + /// Returns a new StylusPointDescription with the common StylusPointProperties from both + /// + /// stylusPointDescription + /// stylusPointDescriptionPreserveInfo + /// The StylusPointProperties from stylusPointDescriptionPreserveInfo will be returned in the new StylusPointDescription + public static StylusPointDescription GetCommonDescription(StylusPointDescription stylusPointDescription, StylusPointDescription stylusPointDescriptionPreserveInfo) + { + ArgumentNullException.ThrowIfNull(stylusPointDescription); + ArgumentNullException.ThrowIfNull(stylusPointDescriptionPreserveInfo); + + + // if a StylusPointDescription is not null, then _stylusPointPropertyInfos is not null. + // + // ignore X, Y, Pressure - they are guaranteed to be the first3 members + // + Debug.Assert(stylusPointDescription._stylusPointPropertyInfos.Length >= 3 && + stylusPointDescription._stylusPointPropertyInfos[0].Id == StylusPointPropertyIds.X && + stylusPointDescription._stylusPointPropertyInfos[1].Id == StylusPointPropertyIds.Y && + stylusPointDescription._stylusPointPropertyInfos[2].Id == StylusPointPropertyIds.NormalPressure); + + Debug.Assert(stylusPointDescriptionPreserveInfo._stylusPointPropertyInfos.Length >= 3 && + stylusPointDescriptionPreserveInfo._stylusPointPropertyInfos[0].Id == StylusPointPropertyIds.X && + stylusPointDescriptionPreserveInfo._stylusPointPropertyInfos[1].Id == StylusPointPropertyIds.Y && + stylusPointDescriptionPreserveInfo._stylusPointPropertyInfos[2].Id == StylusPointPropertyIds.NormalPressure); + + + //add x, y, p + List commonProperties = new List(); + commonProperties.Add(stylusPointDescriptionPreserveInfo._stylusPointPropertyInfos[0]); + commonProperties.Add(stylusPointDescriptionPreserveInfo._stylusPointPropertyInfos[1]); + commonProperties.Add(stylusPointDescriptionPreserveInfo._stylusPointPropertyInfos[2]); + + //add common properties + for (int x = RequiredCountOfProperties; x < stylusPointDescription._stylusPointPropertyInfos.Length; x++) + { + for (int y = RequiredCountOfProperties; y < stylusPointDescriptionPreserveInfo._stylusPointPropertyInfos.Length; y++) + { + if (StylusPointPropertyInfo.AreCompatible(stylusPointDescription._stylusPointPropertyInfos[x], + stylusPointDescriptionPreserveInfo._stylusPointPropertyInfos[y])) + { + commonProperties.Add(stylusPointDescriptionPreserveInfo._stylusPointPropertyInfos[y]); + } + } + } + + return new StylusPointDescription(commonProperties); + } + + /// + /// Returns true if this StylusPointDescription is a subset + /// of the StylusPointDescription passed in + /// + /// stylusPointDescriptionSuperset + /// + public bool IsSubsetOf(StylusPointDescription stylusPointDescriptionSuperset) + { + ArgumentNullException.ThrowIfNull(stylusPointDescriptionSuperset); + if (stylusPointDescriptionSuperset._stylusPointPropertyInfos.Length < _stylusPointPropertyInfos.Length) + { + return false; + } + // + // iterate through our local properties and make sure that the + // superset contains them + // + for (int x = 0; x < _stylusPointPropertyInfos.Length; x++) + { + Guid id = _stylusPointPropertyInfos[x].Id; + if (-1 == stylusPointDescriptionSuperset.IndexOf(id)) + { + return false; + } + } + return true; + } + + /// + /// Returns the index of the given StylusPointProperty, or -1 if none is found + /// + /// propertyId + private int IndexOf(Guid propertyId) + { + for (int x = 0; x < _stylusPointPropertyInfos.Length; x++) + { + if (_stylusPointPropertyInfos[x].Id == propertyId) + { + return x; + } + } + return -1; + } + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointProperties.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointProperties.cs new file mode 100644 index 0000000..820763b --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointProperties.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Windows.Input; +using WpfInk.PresentationCore.System.Windows.Ink; + +namespace WpfInk.PresentationCore.System.Windows.Input.Stylus +{ + /// + /// StylusPointProperties + /// + internal static class StylusPointProperties + { + /// + /// X + /// + public static readonly StylusPointProperty X = + new StylusPointProperty(StylusPointPropertyIds.X, false); + + /// + /// Y + /// + public static readonly StylusPointProperty Y = + new StylusPointProperty(StylusPointPropertyIds.Y, false); + + /// + /// Z + /// + public static readonly StylusPointProperty Z = + new StylusPointProperty(StylusPointPropertyIds.Z, false); + + /// + /// Width + /// + public static readonly StylusPointProperty Width = + new StylusPointProperty(StylusPointPropertyIds.Width, false); + + /// + /// Height + /// + public static readonly StylusPointProperty Height = + new StylusPointProperty(StylusPointPropertyIds.Height, false); + + /// + /// SystemContact + /// + public static readonly StylusPointProperty SystemTouch = + new StylusPointProperty(StylusPointPropertyIds.SystemTouch, false); + + /// + /// PacketStatus + /// + public static readonly StylusPointProperty PacketStatus = + new StylusPointProperty(StylusPointPropertyIds.PacketStatus, false); + + /// + /// SerialNumber + /// + public static readonly StylusPointProperty SerialNumber = + new StylusPointProperty(StylusPointPropertyIds.SerialNumber, false); + + /// + /// NormalPressure + /// + public static readonly StylusPointProperty NormalPressure = + new StylusPointProperty(StylusPointPropertyIds.NormalPressure, false); + + /// + /// TangentPressure + /// + public static readonly StylusPointProperty TangentPressure = + new StylusPointProperty(StylusPointPropertyIds.TangentPressure, false); + + /// + /// ButtonPressure + /// + public static readonly StylusPointProperty ButtonPressure = + new StylusPointProperty(StylusPointPropertyIds.ButtonPressure, false); + + /// + /// XTiltOrientation + /// + public static readonly StylusPointProperty XTiltOrientation = + new StylusPointProperty(StylusPointPropertyIds.XTiltOrientation, false); + + /// + /// YTiltOrientation + /// + public static readonly StylusPointProperty YTiltOrientation = + new StylusPointProperty(StylusPointPropertyIds.YTiltOrientation, false); + + /// + /// AzimuthOrientation + /// + public static readonly StylusPointProperty AzimuthOrientation = + new StylusPointProperty(StylusPointPropertyIds.AzimuthOrientation, false); + + /// + /// AltitudeOrientation + /// + public static readonly StylusPointProperty AltitudeOrientation = + new StylusPointProperty(StylusPointPropertyIds.AltitudeOrientation, false); + + /// + /// TwistOrientation + /// + public static readonly StylusPointProperty TwistOrientation = + new StylusPointProperty(StylusPointPropertyIds.TwistOrientation, false); + + /// + /// PitchRotation + /// + public static readonly StylusPointProperty PitchRotation = + new StylusPointProperty(StylusPointPropertyIds.PitchRotation, false); + + /// + /// RollRotation + /// + public static readonly StylusPointProperty RollRotation = + new StylusPointProperty(StylusPointPropertyIds.RollRotation, false); + + /// + /// YawRotation + /// + public static readonly StylusPointProperty YawRotation = + new StylusPointProperty(StylusPointPropertyIds.YawRotation, false); + + /// + /// TipButton + /// + public static readonly StylusPointProperty TipButton = + new StylusPointProperty(StylusPointPropertyIds.TipButton, true); + + /// + /// BarrelButton + /// + public static readonly StylusPointProperty BarrelButton = + new StylusPointProperty(StylusPointPropertyIds.BarrelButton, true); + + /// + /// SecondaryTipButton + /// + public static readonly StylusPointProperty SecondaryTipButton = + new StylusPointProperty(StylusPointPropertyIds.SecondaryTipButton, true); + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointProperty.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointProperty.cs new file mode 100644 index 0000000..6dd382d --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointProperty.cs @@ -0,0 +1,101 @@ +// 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.Globalization; +using WpfInk.PresentationCore.System.Windows.Ink; + +namespace WpfInk.PresentationCore.System.Windows.Input.Stylus +{ + /// + /// StylusPointProperty + /// + internal class StylusPointProperty + { + /// + /// Instance data + /// + private Guid _id; + private bool _isButton; + + /// + /// StylusPointProperty + /// + /// identifier + /// isButton + public StylusPointProperty(Guid identifier, bool isButton) + { + Initialize(identifier, isButton); + } + + /// + /// StylusPointProperty + /// + /// + /// Protected - used by the StylusPointPropertyInfo ctor + protected StylusPointProperty(StylusPointProperty stylusPointProperty) + { + ArgumentNullException.ThrowIfNull(stylusPointProperty); + Initialize(stylusPointProperty.Id, stylusPointProperty.IsButton); + } + + /// + /// Common ctor helper + /// + /// identifier + /// isButton + private void Initialize(Guid identifier, bool isButton) + { + // + // validate isButton for known guids + // + if (StylusPointPropertyIds.IsKnownButton(identifier)) + { + if (!isButton) + { + //error, this is a known button + throw new ArgumentException(SR.InvalidIsButtonForId, nameof(isButton)); + } + } + else + { + if (StylusPointPropertyIds.IsKnownId(identifier) && isButton) + { + //error, this is a known guid that is NOT a button + throw new ArgumentException(SR.InvalidIsButtonForId2, nameof(isButton)); + } + } + + _id = identifier; + _isButton = isButton; + } + + /// + /// Id + /// + public Guid Id + { + get { return _id; } + } + + /// + /// IsButton + /// + public bool IsButton + { + get { return _isButton; } + } + + /// + /// Returns a human readable string representation + /// + public override string ToString() + { + return "{Id=" + + StylusPointPropertyIds.GetStringRepresentation(_id) + + ", IsButton=" + + _isButton.ToString(CultureInfo.InvariantCulture) + + "}"; + } + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyId.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyId.cs new file mode 100644 index 0000000..80af7ae --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyId.cs @@ -0,0 +1,416 @@ +// 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.Windows.Input; +using System.Collections.Generic; + +namespace WpfInk.PresentationCore.System.Windows.Ink +{ + /// + /// StylusPointPropertyIds + /// + /// + internal static class StylusPointPropertyIds + { + #region Property GUIDs + + /// + /// The x-coordinate in the tablet coordinate space. + /// + /// + public static readonly Guid X = new Guid(0x598A6A8F, 0x52C0, 0x4BA0, 0x93, 0xAF, 0xAF, 0x35, 0x74, 0x11, 0xA5, 0x61); + /// + /// The y-coordinate in the tablet coordinate space. + /// + /// + public static readonly Guid Y = new Guid(0xB53F9F75, 0x04E0, 0x4498, 0xA7, 0xEE, 0xC3, 0x0D, 0xBB, 0x5A, 0x90, 0x11); + /// + /// The z-coordinate or distance of the pen tip from the tablet surface. + /// + /// + public static readonly Guid Z = new Guid(0x735ADB30, 0x0EBB, 0x4788, 0xA0, 0xE4, 0x0F, 0x31, 0x64, 0x90, 0x05, 0x5D); + /// + /// The width value of touch on the tablet surface. + /// + /// + public static readonly Guid Width = new Guid(0xbaabe94d, 0x2712, 0x48f5, 0xbe, 0x9d, 0x8f, 0x8b, 0x5e, 0xa0, 0x71, 0x1a); + /// + /// The height value of touch on the tablet surface. + /// + /// + public static readonly Guid Height = new Guid(0xe61858d2, 0xe447, 0x4218, 0x9d, 0x3f, 0x18, 0x86, 0x5c, 0x20, 0x3d, 0xf4); + /// + /// SystemTouch + /// + /// + public static readonly Guid SystemTouch = new Guid(0xe706c804, 0x57f0, 0x4f00, 0x8a, 0x0c, 0x85, 0x3d, 0x57, 0x78, 0x9b, 0xe9); + /// + /// The current status of the pen pointer. + /// + /// + public static readonly Guid PacketStatus = new Guid(0x6E0E07BF, 0xAFE7, 0x4CF7, 0x87, 0xD1, 0xAF, 0x64, 0x46, 0x20, 0x84, 0x18); + /// + /// Identifies the packet. + /// + /// + public static readonly Guid SerialNumber = new Guid(0x78A81B56, 0x0935, 0x4493, 0xBA, 0xAE, 0x00, 0x54, 0x1A, 0x8A, 0x16, 0xC4); + /// + /// Downward pressure of the pen tip on the tablet surface. + /// + /// + public static readonly Guid NormalPressure = new Guid(0x7307502D, 0xF9F4, 0x4E18, 0xB3, 0xF2, 0x2C, 0xE1, 0xB1, 0xA3, 0x61, 0x0C); + /// + /// Diagonal pressure of the pen tip on the tablet surface. + /// + /// + public static readonly Guid TangentPressure = new Guid(0x6DA4488B, 0x5244, 0x41EC, 0x90, 0x5B, 0x32, 0xD8, 0x9A, 0xB8, 0x08, 0x09); + /// + /// Pressure on a pressure sensitive button. + /// + /// + public static readonly Guid ButtonPressure = new Guid(0x8B7FEFC4, 0x96AA, 0x4BFE, 0xAC, 0x26, 0x8A, 0x5F, 0x0B, 0xE0, 0x7B, 0xF5); + /// + /// The x-tilt orientation is the angle between the y,z-plane and the pen and y-axis plane. + /// + /// + public static readonly Guid XTiltOrientation = new Guid(0xA8D07B3A, 0x8BF0, 0x40B0, 0x95, 0xA9, 0xB8, 0x0A, 0x6B, 0xB7, 0x87, 0xBF); + /// + /// The y-tilt orientation is the angle between the x,z-plane and the pen and x-axis plane. + /// + /// + public static readonly Guid YTiltOrientation = new Guid(0x0E932389, 0x1D77, 0x43AF, 0xAC, 0x00, 0x5B, 0x95, 0x0D, 0x6D, 0x4B, 0x2D); + /// + /// Clockwise rotation of the pen about the z axis through a full circular range. + /// + /// + public static readonly Guid AzimuthOrientation = new Guid(0x029123B4, 0x8828, 0x410B, 0xB2, 0x50, 0xA0, 0x53, 0x65, 0x95, 0xE5, 0xDC); + /// + /// Angle between the axis of the pen and the surface of the tablet. + /// + /// + public static readonly Guid AltitudeOrientation = new Guid(0x82DEC5C7, 0xF6BA, 0x4906, 0x89, 0x4F, 0x66, 0xD6, 0x8D, 0xFC, 0x45, 0x6C); + /// + /// Clockwise rotation of the pen about its own axis. + /// + /// + public static readonly Guid TwistOrientation = new Guid(0x0D324960, 0x13B2, 0x41E4, 0xAC, 0xE6, 0x7A, 0xE9, 0xD4, 0x3D, 0x2D, 0x3B); + /// + /// Identifies whether the tip is above or below a horizontal line that is perpendicular to the writing surface. Requires 3D digitizer. + /// + /// + public static readonly Guid PitchRotation = new Guid(0x7F7E57B7, 0xBE37, 0x4BE1, 0xA3, 0x56, 0x7A, 0x84, 0x16, 0x0E, 0x18, 0x93); + /// + /// Clockwise rotation of the pen about its own axis. Requires 3D digitizer. + /// + /// + public static readonly Guid RollRotation = new Guid(0x5D5D5E56, 0x6BA9, 0x4C5B, 0x9F, 0xB0, 0x85, 0x1C, 0x91, 0x71, 0x4E, 0x56); + /// + /// Yaw identifies whether the tip is turning left or right around the center of its horzontal axis (pen is horizontal). Requires 3D digitizer. + /// + /// + public static readonly Guid YawRotation = new Guid(0x6A849980, 0x7C3A, 0x45B7, 0xAA, 0x82, 0x90, 0xA2, 0x62, 0x95, 0x0E, 0x89); + /// + /// Identifies the tip button of a stylus. Used for identifying StylusButtons in StylusPointDescription. + /// + /// + public static readonly Guid TipButton = new Guid(0x39143d3, 0x78cb, 0x449c, 0xa8, 0xe7, 0x67, 0xd1, 0x88, 0x64, 0xc3, 0x32); + /// + /// Identifies the button on the barrel of a stylus. Used for identifying StylusButtons in StylusPointDescription. + /// + /// + public static readonly Guid BarrelButton = new Guid(0xf0720328, 0x663b, 0x418f, 0x85, 0xa6, 0x95, 0x31, 0xae, 0x3e, 0xcd, 0xfa); + /// + /// Identifies the secondary tip barrel button of a stylus. Used for identifying StylusButtons in StylusPointDescription. + /// + /// + public static readonly Guid SecondaryTipButton = new Guid(0x67743782, 0xee5, 0x419a, 0xa1, 0x2b, 0x27, 0x3a, 0x9e, 0xc0, 0x8f, 0x3d); + + #endregion + + #region HID Constants + + /// + /// + /// WM_POINTER stack must parse out HID spec usage pages + /// + /// + internal enum HidUsagePage + { + Undefined = 0x00, + Generic = 0x01, + Simulation = 0x02, + Vr = 0x03, + Sport = 0x04, + Game = 0x05, + Keyboard = 0x07, + Led = 0x08, + Button = 0x09, + Ordinal = 0x0a, + Telephony = 0x0b, + Consumer = 0x0c, + Digitizer = 0x0d, + Unicode = 0x10, + Alphanumeric = 0x14, + BarcodeScanner = 0x8C, + WeighingDevice = 0x8D, + MagneticStripeReader = 0x8E, + CameraControl = 0x90, + MicrosoftBluetoothHandsfree = 0xfff3, + } + + /// + /// + /// + /// WISP pre-parsed these, WM_POINTER stack must do it itself + /// + /// See Stylus\biblio.txt - 1 + /// + /// + internal enum HidUsage + { + TipPressure = 0x30, + X = 0x30, + BarrelPressure = 0x31, + Y = 0x31, + Z = 0x32, + XTilt = 0x3D, + YTilt = 0x3E, + Azimuth = 0x3F, + Altitude = 0x40, + Twist = 0x41, + TipSwitch = 0x42, + SecondaryTipSwitch = 0x43, + BarrelSwitch = 0x44, + TouchConfidence = 0x47, + Width = 0x48, + Height = 0x49, + TransducerSerialNumber = 0x5B, + } + + #endregion + + #region HID Associations + + /// + /// + /// WM_POINTER stack usage preparation based on associations maintained from the legacy WISP based stack + /// + private static Dictionary> _hidToGuidMap = new Dictionary>() + { + { HidUsagePage.Generic, + new Dictionary() + { + { HidUsage.X, X }, + { HidUsage.Y, Y }, + { HidUsage.Z, Z }, + } + }, + { HidUsagePage.Digitizer, + new Dictionary() + { + { HidUsage.Width, Width }, + { HidUsage.Height, Height }, + { HidUsage.TouchConfidence, SystemTouch }, + { HidUsage.TipPressure, NormalPressure }, + { HidUsage.BarrelPressure, ButtonPressure }, + { HidUsage.XTilt, XTiltOrientation }, + { HidUsage.YTilt, YTiltOrientation }, + { HidUsage.Azimuth, AzimuthOrientation }, + { HidUsage.Altitude, AltitudeOrientation }, + { HidUsage.Twist, TwistOrientation }, + { HidUsage.TipSwitch, TipButton }, + { HidUsage.SecondaryTipSwitch, SecondaryTipButton }, + { HidUsage.BarrelSwitch, BarrelButton }, + { HidUsage.TransducerSerialNumber, SerialNumber }, + } + }, + }; + + #endregion + + #region Utility Functions + + /// + /// Retrieves the GUID of the stylus property associated with the usage page and usage ids + /// within the HID specification. + /// + /// The usage page id of the HID specification + /// The usage id of the HID specification + /// + /// If known, the GUID associated with the usagePageId and usageId. + /// If not known, GUID.Empty + /// + internal static Guid GetKnownGuid(HidUsagePage page, HidUsage usage) + { + Guid result = Guid.Empty; + + Dictionary pageMap = null; + + if (_hidToGuidMap.TryGetValue(page, out pageMap)) + { + pageMap.TryGetValue(usage, out result); + } + + return result; + } + + /// + /// Called by the StylusPointProperty constructor. + /// Any new Guids in this static class should be added here + /// + /// guid + internal static bool IsKnownId(Guid guid) + { + if (guid == X || + guid == Y || + guid == Z || + guid == Width || + guid == Height || + guid == SystemTouch || + guid == PacketStatus || + guid == SerialNumber || + guid == NormalPressure || + guid == TangentPressure || + guid == ButtonPressure || + guid == XTiltOrientation || + guid == YTiltOrientation || + guid == AzimuthOrientation || + guid == AltitudeOrientation || + guid == TwistOrientation || + guid == PitchRotation || + guid == RollRotation || + guid == YawRotation || + guid == TipButton || + guid == BarrelButton || + guid == SecondaryTipButton) + { + return true; + } + return false; + } + + /// + /// Called by the StylusPointProperty constructor. + /// Any new Guids in this static class should be added here + /// + /// guid + internal static string GetStringRepresentation(Guid guid) + { + if (guid == X) + { + return "X"; + } + if (guid == Y) + { + return "Y"; + } + if (guid == Z) + { + return "Z"; + } + if (guid == Width) + { + return "Width"; + } + if (guid == Height) + { + return "Height"; + } + if (guid == SystemTouch) + { + return "SystemTouch"; + } + if (guid == PacketStatus) + { + return "PacketStatus"; + } + if (guid == SerialNumber) + { + return "SerialNumber"; + } + if (guid == NormalPressure) + { + return "NormalPressure"; + } + if (guid == TangentPressure) + { + return "TangentPressure"; + } + if (guid == ButtonPressure) + { + return "ButtonPressure"; + } + if (guid == XTiltOrientation) + { + return "XTiltOrientation"; + } + if (guid == YTiltOrientation) + { + return "YTiltOrientation"; + } + if (guid == AzimuthOrientation) + { + return "AzimuthOrientation"; + } + if (guid == AltitudeOrientation) + { + return "AltitudeOrientation"; + } + if (guid == TwistOrientation) + { + return "TwistOrientation"; + } + if (guid == PitchRotation) + { + return "PitchRotation"; + } + if (guid == RollRotation) + { + return "RollRotation"; + } + if (guid == AltitudeOrientation) + { + return "AltitudeOrientation"; + } + if (guid == YawRotation) + { + return "YawRotation"; + } + if (guid == TipButton) + { + return "TipButton"; + } + if (guid == BarrelButton) + { + return "BarrelButton"; + } + if (guid == SecondaryTipButton) + { + return "SecondaryTipButton"; + } + return "Unknown"; + } + + /// + /// Called by the StylusPointProperty constructor. + /// Any new button Guids in this static class should be added here + /// + /// guid + internal static bool IsKnownButton(Guid guid) + { + if (guid == TipButton || + guid == BarrelButton || + guid == SecondaryTipButton) + { + return true; + } + return false; + } + + #endregion + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyInfo.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyInfo.cs new file mode 100644 index 0000000..def55f5 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyInfo.cs @@ -0,0 +1,130 @@ +// 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.ComponentModel; +using System.Diagnostics; +using System.Windows.Input; +using WpfInk.PresentationCore.System.Windows.Ink; + +namespace WpfInk.PresentationCore.System.Windows.Input.Stylus +{ + /// + /// StylusPointPropertyInfo + /// + internal class StylusPointPropertyInfo : StylusPointProperty + { + /// + /// Instance data + /// + private int _min; + private int _max; + private float _resolution; + private StylusPointPropertyUnit _unit; + + /// + /// For a given StylusPointProperty, instantiates a StylusPointPropertyInfo with default values + /// + /// + public StylusPointPropertyInfo(StylusPointProperty stylusPointProperty) + : base(stylusPointProperty) //base checks for null + { + StylusPointPropertyInfo info = + StylusPointPropertyInfoDefaults.GetStylusPointPropertyInfoDefault(stylusPointProperty); + _min = info.Minimum; + _max = info.Maximum; + _resolution = info.Resolution; + _unit = info.Unit; + } + + /// + /// StylusPointProperty + /// + /// + /// minimum + /// maximum + /// unit + /// resolution + public StylusPointPropertyInfo(StylusPointProperty stylusPointProperty, int minimum, int maximum, StylusPointPropertyUnit unit, float resolution) + : base(stylusPointProperty) //base checks for null + { + // validate unit + if (!StylusPointPropertyUnitHelper.IsDefined(unit)) + { + throw new InvalidEnumArgumentException("unit", (int) unit, typeof(StylusPointPropertyUnit)); + } + + // validate min/max + if (maximum < minimum) + { + throw new ArgumentException(SR.Stylus_InvalidMax, nameof(maximum)); + } + + // validate resolution + if (resolution < 0.0f) + { + throw new ArgumentException(SR.InvalidStylusPointPropertyInfoResolution, nameof(resolution)); + } + + _min = minimum; + _max = maximum; + _resolution = resolution; + _unit = unit; + } + + /// + /// Minimum + /// + public int Minimum + { + get { return _min; } + } + + /// + /// Maximum + /// + public int Maximum + { + get { return _max; } + } + + /// + /// Resolution + /// + public float Resolution + { + get { return _resolution; } + internal set { _resolution = value; } + } + + /// + /// Unit + /// + public StylusPointPropertyUnit Unit + { + get { return _unit; } + } + + /// + /// Internal helper method for comparing compat for two StylusPointPropertyInfos + /// + internal static bool AreCompatible(StylusPointPropertyInfo stylusPointPropertyInfo1, StylusPointPropertyInfo stylusPointPropertyInfo2) + { + if (stylusPointPropertyInfo1 == null || stylusPointPropertyInfo2 == null) + { + throw new ArgumentNullException("stylusPointPropertyInfo"); + } + + Debug.Assert((stylusPointPropertyInfo1.Id != StylusPointPropertyIds.X && + stylusPointPropertyInfo1.Id != StylusPointPropertyIds.Y && + stylusPointPropertyInfo2.Id != StylusPointPropertyIds.X && + stylusPointPropertyInfo2.Id != StylusPointPropertyIds.Y), + "Why are you checking X, Y for compatibility? They're always compatible"); + // + // we only take ID and IsButton into account, we don't take metrics into account + // + return (stylusPointPropertyInfo1.Id == stylusPointPropertyInfo2.Id && + stylusPointPropertyInfo1.IsButton == stylusPointPropertyInfo2.IsButton); + } + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyInfoDefaults.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyInfoDefaults.cs new file mode 100644 index 0000000..6c8e0cd --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyInfoDefaults.cs @@ -0,0 +1,363 @@ +// 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.Windows.Input; +using System.Collections.Generic; +using WpfInk.PresentationCore.System.Windows.Input.Stylus; + +namespace WpfInk.PresentationCore.System.Windows.Ink +{ + internal static class StylusPointPropertyInfoDefaults + { + /// + /// X + /// + internal static readonly StylusPointPropertyInfo X = + new StylusPointPropertyInfo(StylusPointProperties.X, + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.Centimeters, + 1000.0f); + + /// + /// Y + /// + internal static readonly StylusPointPropertyInfo Y = + new StylusPointPropertyInfo(StylusPointProperties.Y, + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.Centimeters, + 1000.0f); + + /// + /// Z + /// + internal static readonly StylusPointPropertyInfo Z = + new StylusPointPropertyInfo(StylusPointProperties.Z, + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.Centimeters, + 1000.0f); + + /// + /// Width + /// + internal static readonly StylusPointPropertyInfo Width = + new StylusPointPropertyInfo(StylusPointProperties.Width, + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.Centimeters, + 1000.0f); + + /// + /// Height + /// + internal static readonly StylusPointPropertyInfo Height = + new StylusPointPropertyInfo(StylusPointProperties.Height, + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.Centimeters, + 1000.0f); + + /// + /// SystemTouch + /// + internal static readonly StylusPointPropertyInfo SystemTouch = + new StylusPointPropertyInfo(StylusPointProperties.SystemTouch, + 0, + 1, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// PacketStatus + /// + internal static readonly StylusPointPropertyInfo PacketStatus = + new StylusPointPropertyInfo(StylusPointProperties.PacketStatus, + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// SerialNumber + /// + /// + internal static readonly StylusPointPropertyInfo SerialNumber = + new StylusPointPropertyInfo(StylusPointProperties.SerialNumber, + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// NormalPressure + /// + internal static readonly StylusPointPropertyInfo NormalPressure = + new StylusPointPropertyInfo(StylusPointProperties.NormalPressure, + 0, + 1023, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// TangentPressure + /// + internal static readonly StylusPointPropertyInfo TangentPressure = + new StylusPointPropertyInfo(StylusPointProperties.TangentPressure, + 0, + 1023, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// ButtonPressure + /// + internal static readonly StylusPointPropertyInfo ButtonPressure = + new StylusPointPropertyInfo(StylusPointProperties.ButtonPressure, + 0, + 1023, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// XTiltOrientation + /// + internal static readonly StylusPointPropertyInfo XTiltOrientation = + new StylusPointPropertyInfo(StylusPointProperties.XTiltOrientation, + 0, + 3600, + StylusPointPropertyUnit.Degrees, + 10.0f); + + /// + /// YTiltOrientation + /// + internal static readonly StylusPointPropertyInfo YTiltOrientation = + new StylusPointPropertyInfo(StylusPointProperties.YTiltOrientation, + 0, + 3600, + StylusPointPropertyUnit.Degrees, + 10.0f); + + /// + /// AzimuthOrientation + /// + internal static readonly StylusPointPropertyInfo AzimuthOrientation = + new StylusPointPropertyInfo(StylusPointProperties.AzimuthOrientation, + 0, + 3600, + StylusPointPropertyUnit.Degrees, + 10.0f); + + /// + /// AltitudeOrientation + /// + internal static readonly StylusPointPropertyInfo AltitudeOrientation = + new StylusPointPropertyInfo(StylusPointProperties.AltitudeOrientation, + -900, + 900, + StylusPointPropertyUnit.Degrees, + 10.0f); + + /// + /// TwistOrientation + /// + internal static readonly StylusPointPropertyInfo TwistOrientation = + new StylusPointPropertyInfo(StylusPointProperties.TwistOrientation, + 0, + 3600, + StylusPointPropertyUnit.Degrees, + 10.0f); + + /// + /// PitchRotation + /// + internal static readonly StylusPointPropertyInfo PitchRotation = + new StylusPointPropertyInfo(StylusPointProperties.PitchRotation, + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// RollRotation + /// + internal static readonly StylusPointPropertyInfo RollRotation = + new StylusPointPropertyInfo(StylusPointProperties.RollRotation, + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// YawRotation + /// + internal static readonly StylusPointPropertyInfo YawRotation = + new StylusPointPropertyInfo(StylusPointProperties.YawRotation, + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// TipButton + /// + internal static readonly StylusPointPropertyInfo TipButton = + new StylusPointPropertyInfo(StylusPointProperties.TipButton, + 0, + 1, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// BarrelButton + /// + internal static readonly StylusPointPropertyInfo BarrelButton = + new StylusPointPropertyInfo(StylusPointProperties.BarrelButton, + 0, + 1, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// SecondaryTipButton + /// + internal static readonly StylusPointPropertyInfo SecondaryTipButton = + new StylusPointPropertyInfo(StylusPointProperties.SecondaryTipButton, + 0, + 1, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// Default Value + /// + internal static readonly StylusPointPropertyInfo DefaultValue = + new StylusPointPropertyInfo(new StylusPointProperty(Guid.NewGuid(), false), + Int32.MinValue, + Int32.MaxValue, + StylusPointPropertyUnit.None, + 1.0F); + + /// + /// DefaultButton + /// + internal static readonly StylusPointPropertyInfo DefaultButton = + new StylusPointPropertyInfo(new StylusPointProperty(Guid.NewGuid(), true), + 0, + 1, + StylusPointPropertyUnit.None, + 1.0f); + + /// + /// For a given StylusPointProperty, return the default property info + /// + /// stylusPointProperty + /// + internal static StylusPointPropertyInfo GetStylusPointPropertyInfoDefault(StylusPointProperty stylusPointProperty) + { + if (stylusPointProperty.Id == StylusPointPropertyIds.X) + { + return StylusPointPropertyInfoDefaults.X; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.Y) + { + return StylusPointPropertyInfoDefaults.Y; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.Z) + { + return StylusPointPropertyInfoDefaults.Z; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.Width) + { + return StylusPointPropertyInfoDefaults.Width; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.Height) + { + return StylusPointPropertyInfoDefaults.Height; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.SystemTouch) + { + return StylusPointPropertyInfoDefaults.SystemTouch; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.PacketStatus) + { + return StylusPointPropertyInfoDefaults.PacketStatus; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.SerialNumber) + { + return StylusPointPropertyInfoDefaults.SerialNumber; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.NormalPressure) + { + return StylusPointPropertyInfoDefaults.NormalPressure; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.TangentPressure) + { + return StylusPointPropertyInfoDefaults.TangentPressure; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.ButtonPressure) + { + return StylusPointPropertyInfoDefaults.ButtonPressure; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.XTiltOrientation) + { + return StylusPointPropertyInfoDefaults.XTiltOrientation; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.YTiltOrientation) + { + return StylusPointPropertyInfoDefaults.YTiltOrientation; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.AzimuthOrientation) + { + return StylusPointPropertyInfoDefaults.AzimuthOrientation; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.AltitudeOrientation) + { + return StylusPointPropertyInfoDefaults.AltitudeOrientation; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.TwistOrientation) + { + return StylusPointPropertyInfoDefaults.TwistOrientation; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.PitchRotation) + { + return StylusPointPropertyInfoDefaults.PitchRotation; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.RollRotation) + { + return StylusPointPropertyInfoDefaults.RollRotation; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.YawRotation) + { + return StylusPointPropertyInfoDefaults.YawRotation; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.TipButton) + { + return StylusPointPropertyInfoDefaults.TipButton; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.BarrelButton) + { + return StylusPointPropertyInfoDefaults.BarrelButton; + } + if (stylusPointProperty.Id == StylusPointPropertyIds.SecondaryTipButton) + { + return StylusPointPropertyInfoDefaults.SecondaryTipButton; + } + + // + // return a default + // + if (stylusPointProperty.IsButton) + { + return StylusPointPropertyInfoDefaults.DefaultButton; + } + return StylusPointPropertyInfoDefaults.DefaultValue; + } + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyUnit.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyUnit.cs new file mode 100644 index 0000000..a723eaa --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Input/Stylus/StylusPointPropertyUnit.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace WpfInk.PresentationCore.System.Windows.Input.Stylus +{ + /// + /// Stylus data is made up of n number of properties. Each property can contain one or more + /// values such as x or y coordinate or button states. + /// This enum defines the various possible units for the values in the stylus data + /// + /// + internal enum StylusPointPropertyUnit + { + /// Specifies that the units are unknown. + /// + None = 0, + /// Specifies that the property value is in inches (distance units). + /// + Inches = 1, + /// Specifies that the property value is in centimeters (distance units). + /// + Centimeters = 2, + /// Specifies that the property value is in degrees (angle units). + /// + Degrees = 3, + /// Specifies that the property value is in radians (angle units). + /// + Radians = 4, + /// Specifies that the property value is in seconds (angle units). + /// + Seconds = 5, + /// + /// Specifies that the property value is in pounds (force, or mass, units). + Pounds = 6, + /// + /// Specifies that the property value is in grams (force, or mass, units). + Grams = 7 + } + + /// + /// Used to validate the enum + /// + /// + /// Added various functions to support WM_POINTER based stack + /// + internal static class StylusPointPropertyUnitHelper + { + #region Constants + + /// + /// Mask to extract units from raw WM_POINTER data + /// + /// + private const uint UNIT_MASK = 0x000F; + + #endregion + + #region Conversion Maps + + /// + /// Mapping for WM_POINTER based unit, taken from legacy WISP code + /// + private static Dictionary _pointerUnitMap = new Dictionary() + { + { 1, StylusPointPropertyUnit.Centimeters }, + { 2, StylusPointPropertyUnit.Radians }, + { 3, StylusPointPropertyUnit.Inches }, + { 4, StylusPointPropertyUnit.Degrees }, + }; + + #endregion + + #region Utility Functions + + /// + /// Convert WM_POINTER units to WPF units + /// + /// + /// + internal static StylusPointPropertyUnit? FromPointerUnit(uint pointerUnit) + { + StylusPointPropertyUnit unit = StylusPointPropertyUnit.None; + + _pointerUnitMap.TryGetValue(pointerUnit & UNIT_MASK, out unit); + + return (IsDefined(unit)) ? unit : (StylusPointPropertyUnit?) null; + } + + internal static bool IsDefined(StylusPointPropertyUnit unit) + { + if (unit >= StylusPointPropertyUnit.None && unit <= StylusPointPropertyUnit.Grams) + { + return true; + } + return false; + } + + #endregion + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Point.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Point.cs new file mode 100644 index 0000000..ac80e78 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Point.cs @@ -0,0 +1,418 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// +// +// This file was generated, please do not edit it directly. +// +// Please see MilCodeGen.html for more information. +// + + +//using System.Windows.Converters; + +using System; + +namespace WpfInk.PresentationCore.System.Windows +{ + internal struct Point + { + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + + #region Public Methods + + + + + /// + /// Compares two Point 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. + /// + /// + /// bool - true if the two Point instances are exactly equal, false otherwise + /// + /// The first Point to compare + /// The second Point to compare + public static bool operator ==(Point point1, Point point2) + { + return point1.X == point2.X && + point1.Y == point2.Y; + } + + /// + /// Compares two Point 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 Point instances are exactly unequal, false otherwise + /// + /// The first Point to compare + /// The second Point to compare + public static bool operator !=(Point point1, Point point2) + { + return !(point1 == point2); + } + /// + /// Compares two Point instances for object equality. 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 the two Point instances are exactly equal, false otherwise + /// + /// The first Point to compare + /// The second Point to compare + public static bool Equals(Point point1, Point point2) + { + return point1.X.Equals(point2.X) && + point1.Y.Equals(point2.Y); + } + + /// + /// Equals - compares this Point 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 the object is an instance of Point and if it's equal to "this". + /// + /// The object to compare to "this" + public override bool Equals(object o) + { + if ((null == o) || !(o is Point)) + { + return false; + } + + Point value = (Point) o; + return Point.Equals(this, value); + } + + /// + /// Equals - compares this Point 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 Point to compare to "this" + public bool Equals(Point value) + { + return Point.Equals(this, value); + } + /// + /// Returns the HashCode for this Point + /// + /// + /// int - the HashCode for this Point + /// + public override int GetHashCode() + { + // Perform field-by-field XOR of HashCodes + return X.GetHashCode() ^ + Y.GetHashCode(); + } + + + + #endregion Public Methods + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + + + + #region Public Properties + + /// + /// X - double. Default value is 0. + /// + public double X + { + get + { + return _x; + } + + set + { + _x = value; + } + + } + + /// + /// Y - double. Default value is 0. + /// + public double Y + { + get + { + return _y; + } + + set + { + _y = value; + } + + } + + #endregion Public Properties + + //------------------------------------------------------ + // + // Protected Methods + // + //------------------------------------------------------ + + #region Protected Methods + + + + + + #endregion ProtectedMethods + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + #region Internal Methods + + + + + + + + + + #endregion Internal Methods + + //------------------------------------------------------ + // + // Internal Properties + // + //------------------------------------------------------ + + #region Internal Properties + + + /// + /// Creates a string representation of this object based on the current culture. + /// + /// + /// A string representation of this object. + /// + public override string ToString() + { + return $"({X},{Y})"; + } + + + + + #endregion Internal Properties + + //------------------------------------------------------ + // + // Dependency Properties + // + //------------------------------------------------------ + + #region Dependency Properties + + + + #endregion Dependency Properties + + //------------------------------------------------------ + // + // Internal Fields + // + //------------------------------------------------------ + + #region Internal Fields + + + internal double _x; + internal double _y; + + + + + #endregion Internal Fields + + + + #region Constructors + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + + + + #endregion Constructors + + #region Constructors + + /// + /// Constructor which accepts the X and Y values + /// + /// The value for the X coordinate of the new Point + /// The value for the Y coordinate of the new Point + public Point(double x, double y) + { + _x = x; + _y = y; + } + + #endregion Constructors + + #region Public Methods + + /// + /// Offset - update the location by adding offsetX to X and offsetY to Y + /// + /// The offset in the x dimension + /// The offset in the y dimension + public void Offset(double offsetX, double offsetY) + { + _x += offsetX; + _y += offsetY; + } + + /// + /// Operator Point + Vector + /// + /// + /// Point - The result of the addition + /// + /// The Point to be added to the Vector + /// The Vectr to be added to the Point + public static Point operator +(Point point, Vector vector) + { + return new Point(point._x + vector._x, point._y + vector._y); + } + + /// + /// Add: Point + Vector + /// + /// + /// Point - The result of the addition + /// + /// The Point to be added to the Vector + /// The Vector to be added to the Point + public static Point Add(Point point, Vector vector) + { + return new Point(point._x + vector._x, point._y + vector._y); + } + + /// + /// Operator Point - Vector + /// + /// + /// Point - The result of the subtraction + /// + /// The Point from which the Vector is subtracted + /// The Vector which is subtracted from the Point + public static Point operator -(Point point, Vector vector) + { + return new Point(point._x - vector._x, point._y - vector._y); + } + + /// + /// Subtract: Point - Vector + /// + /// + /// Point - The result of the subtraction + /// + /// The Point from which the Vector is subtracted + /// The Vector which is subtracted from the Point + public static Point Subtract(Point point, Vector vector) + { + return new Point(point._x - vector._x, point._y - vector._y); + } + + /// + /// Operator Point - Point + /// + /// + /// Vector - The result of the subtraction + /// + /// The Point from which point2 is subtracted + /// The Point subtracted from point1 + public static Vector operator -(Point point1, Point point2) + { + return new Vector(point1._x - point2._x, point1._y - point2._y); + } + + /// + /// Subtract: Point - Point + /// + /// + /// Vector - The result of the subtraction + /// + /// The Point from which point2 is subtracted + /// The Point subtracted from point1 + public static Vector Subtract(Point point1, Point point2) + { + return new Vector(point1._x - point2._x, point1._y - point2._y); + } + + + + /// + /// Explicit conversion to Size. Note that since Size cannot contain negative values, + /// the resulting size will contains the absolute values of X and Y + /// + /// + /// Size - A Size equal to this Point + /// + /// Point - the Point to convert to a Size + public static explicit operator Size(Point point) + { + return new Size(Math.Abs(point._x), Math.Abs(point._y)); + } + + /// + /// Explicit conversion to Vector + /// + /// + /// Vector - A Vector equal to this Point + /// + /// Point - the Point to convert to a Vector + public static explicit operator Vector(Point point) + { + return new Vector(point._x, point._y); + } + + #endregion Public Methods + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Rect.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Rect.cs new file mode 100644 index 0000000..1963f82 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Rect.cs @@ -0,0 +1,1111 @@ +// 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.Diagnostics; + +namespace WpfInk.PresentationCore.System.Windows +{ + internal struct Rect + { + #region Constructors + + /// + /// Constructor which sets the initial values to the values of the parameters + /// + public Rect(Point location, + Size size) + { + if (size.IsEmpty) + { + this = s_empty; + } + else + { + _x = location._x; + _y = location._y; + _width = size._width; + _height = size._height; + } + } + + /// + /// Constructor which sets the initial values to the values of the parameters. + /// Width and Height must be non-negative + /// + public Rect(double x, + double y, + double width, + double height) + { + if (width < 0 || height < 0) + { + throw new global::System.ArgumentException(SR.Size_WidthAndHeightCannotBeNegative); + } + + _x = x; + _y = y; + _width = width; + _height = height; + } + + /// + /// Constructor which sets the initial values to bound the two points provided. + /// + public Rect(Point point1, + Point point2) + { + _x = Math.Min(point1._x, point2._x); + _y = Math.Min(point1._y, point2._y); + + // Max with 0 to prevent double weirdness from causing us to be (-epsilon..0) + _width = Math.Max(Math.Max(point1._x, point2._x) - _x, 0); + _height = Math.Max(Math.Max(point1._y, point2._y) - _y, 0); + } + + /// + /// Constructor which sets the initial values to bound the point provided and the point + /// which results from point + vector. + /// + public Rect(Point point, + Vector vector) : this(point, point + vector) + { + } + + /// + /// Constructor which sets the initial values to bound the (0,0) point and the point + /// that results from (0,0) + size. + /// + public Rect(Size size) + { + if (size.IsEmpty) + { + this = s_empty; + } + else + { + _x = _y = 0; + _width = size.Width; + _height = size.Height; + } + } + + #endregion Constructors + + #region Statics + + /// + /// Empty - a static property which provides an Empty rectangle. X and Y are positive-infinity + /// and Width and Height are negative infinity. This is the only situation where Width or + /// Height can be negative. + /// + public static Rect Empty + { + get + { + return s_empty; + } + } + + #endregion Statics + + #region Public Properties + + /// + /// IsEmpty - this returns true if this rect is the Empty rectangle. + /// Note: If width or height are 0 this Rectangle still contains a 0 or 1 dimensional set + /// of points, so this method should not be used to check for 0 area. + /// + public bool IsEmpty + { + get + { + // The funny width and height tests are to handle NaNs + Debug.Assert((!(_width < 0) && !(_height < 0)) || (this == Empty)); + + return _width < 0; + } + } + + /// + /// Location - The Point representing the origin of the Rectangle + /// + public Point Location + { + get + { + return new Point(_x, _y); + } + set + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Rect_CannotModifyEmptyRect); + } + + _x = value._x; + _y = value._y; + } + } + + /// + /// Size - The Size representing the area of the Rectangle + /// + public Size Size + { + get + { + if (IsEmpty) + return Size.Empty; + return new Size(_width, _height); + } + set + { + if (value.IsEmpty) + { + this = s_empty; + } + else + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Rect_CannotModifyEmptyRect); + } + + _width = value._width; + _height = value._height; + } + } + } + + /// + /// X - The X coordinate of the Location. + /// If this is the empty rectangle, the value will be positive infinity. + /// If this rect is Empty, setting this property is illegal. + /// + public double X + { + get + { + return _x; + } + set + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Rect_CannotModifyEmptyRect); + } + + _x = value; + } + } + + /// + /// Y - The Y coordinate of the Location + /// If this is the empty rectangle, the value will be positive infinity. + /// If this rect is Empty, setting this property is illegal. + /// + public double Y + { + get + { + return _y; + } + set + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Rect_CannotModifyEmptyRect); + } + + _y = value; + } + } + + /// + /// Width - The Width component of the Size. This cannot be set to negative, and will only + /// be negative if this is the empty rectangle, in which case it will be negative infinity. + /// If this rect is Empty, setting this property is illegal. + /// + public double Width + { + get + { + return _width; + } + set + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Rect_CannotModifyEmptyRect); + } + + if (value < 0) + { + throw new global::System.ArgumentException(SR.Size_WidthCannotBeNegative); + } + + _width = value; + } + } + + /// + /// Height - The Height component of the Size. This cannot be set to negative, and will only + /// be negative if this is the empty rectangle, in which case it will be negative infinity. + /// If this rect is Empty, setting this property is illegal. + /// + public double Height + { + get + { + return _height; + } + set + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Rect_CannotModifyEmptyRect); + } + + if (value < 0) + { + throw new global::System.ArgumentException(SR.Size_HeightCannotBeNegative); + } + + _height = value; + } + } + + /// + /// Left Property - This is a read-only alias for X + /// If this is the empty rectangle, the value will be positive infinity. + /// + public double Left + { + get + { + return _x; + } + } + + /// + /// Top Property - This is a read-only alias for Y + /// If this is the empty rectangle, the value will be positive infinity. + /// + public double Top + { + get + { + return _y; + } + } + + /// + /// Right Property - This is a read-only alias for X + Width + /// If this is the empty rectangle, the value will be negative infinity. + /// + public double Right + { + get + { + if (IsEmpty) + { + return Double.NegativeInfinity; + } + + return _x + _width; + } + } + + /// + /// Bottom Property - This is a read-only alias for Y + Height + /// If this is the empty rectangle, the value will be negative infinity. + /// + public double Bottom + { + get + { + if (IsEmpty) + { + return Double.NegativeInfinity; + } + + return _y + _height; + } + } + + /// + /// TopLeft Property - This is a read-only alias for the Point which is at X, Y + /// If this is the empty rectangle, the value will be positive infinity, positive infinity. + /// + public Point TopLeft + { + get + { + return new Point(Left, Top); + } + } + + /// + /// TopRight Property - This is a read-only alias for the Point which is at X + Width, Y + /// If this is the empty rectangle, the value will be negative infinity, positive infinity. + /// + public Point TopRight + { + get + { + return new Point(Right, Top); + } + } + + /// + /// BottomLeft Property - This is a read-only alias for the Point which is at X, Y + Height + /// If this is the empty rectangle, the value will be positive infinity, negative infinity. + /// + public Point BottomLeft + { + get + { + return new Point(Left, Bottom); + } + } + + /// + /// BottomRight Property - This is a read-only alias for the Point which is at X + Width, Y + Height + /// If this is the empty rectangle, the value will be negative infinity, negative infinity. + /// + public Point BottomRight + { + get + { + return new Point(Right, Bottom); + } + } + #endregion Public Properties + + #region Public Methods + + /// + /// Contains - Returns true if the Point is within the rectangle, inclusive of the edges. + /// Returns false otherwise. + /// + /// The point which is being tested + /// + /// Returns true if the Point is within the rectangle. + /// Returns false otherwise + /// + public bool Contains(Point point) + { + return Contains(point._x, point._y); + } + + /// + /// Contains - Returns true if the Point represented by x,y is within the rectangle inclusive of the edges. + /// Returns false otherwise. + /// + /// X coordinate of the point which is being tested + /// Y coordinate of the point which is being tested + /// + /// Returns true if the Point represented by x,y is within the rectangle. + /// Returns false otherwise. + /// + public bool Contains(double x, double y) + { + if (IsEmpty) + { + return false; + } + + return ContainsInternal(x, y); + } + + /// + /// Contains - Returns true if the Rect non-Empty and is entirely contained within the + /// rectangle, inclusive of the edges. + /// Returns false otherwise + /// + public bool Contains(Rect rect) + { + if (IsEmpty || rect.IsEmpty) + { + return false; + } + + return (_x <= rect._x && + _y <= rect._y && + _x + _width >= rect._x + rect._width && + _y + _height >= rect._y + rect._height); + } + + /// + /// IntersectsWith - Returns true if the Rect intersects with this rectangle + /// Returns false otherwise. + /// Note that if one edge is coincident, this is considered an intersection. + /// + /// + /// Returns true if the Rect intersects with this rectangle + /// Returns false otherwise. + /// or Height + /// + /// Rect + public bool IntersectsWith(Rect rect) + { + if (IsEmpty || rect.IsEmpty) + { + return false; + } + + return (rect.Left <= Right) && + (rect.Right >= Left) && + (rect.Top <= Bottom) && + (rect.Bottom >= Top); + } + + /// + /// Intersect - Update this rectangle to be the intersection of this and rect + /// If either this or rect are Empty, the result is Empty as well. + /// + /// The rect to intersect with this + public void Intersect(Rect rect) + { + if (!this.IntersectsWith(rect)) + { + this = Empty; + } + else + { + double left = Math.Max((double) Left, rect.Left); + double top = Math.Max((double) Top, rect.Top); + + // Max with 0 to prevent double weirdness from causing us to be (-epsilon..0) + _width = Math.Max(Math.Min((double) Right, rect.Right) - left, 0); + _height = Math.Max(Math.Min((double) Bottom, rect.Bottom) - top, 0); + + _x = left; + _y = top; + } + } + + /// + /// Intersect - Return the result of the intersection of rect1 and rect2. + /// If either this or rect are Empty, the result is Empty as well. + /// + public static Rect Intersect(Rect rect1, Rect rect2) + { + rect1.Intersect(rect2); + return rect1; + } + + /// + /// Union - Update this rectangle to be the union of this and rect. + /// + public void Union(Rect rect) + { + if (IsEmpty) + { + this = rect; + } + else if (!rect.IsEmpty) + { + double left = Math.Min((double) Left, rect.Left); + double top = Math.Min((double) Top, rect.Top); + + + // We need this check so that the math does not result in NaN + if ((rect.Width == Double.PositiveInfinity) || (Width == Double.PositiveInfinity)) + { + _width = Double.PositiveInfinity; + } + else + { + // Max with 0 to prevent double weirdness from causing us to be (-epsilon..0) + double maxRight = Math.Max((double) Right, rect.Right); + _width = Math.Max(maxRight - left, 0); + } + + // We need this check so that the math does not result in NaN + if ((rect.Height == Double.PositiveInfinity) || (Height == Double.PositiveInfinity)) + { + _height = Double.PositiveInfinity; + } + else + { + // Max with 0 to prevent double weirdness from causing us to be (-epsilon..0) + double maxBottom = Math.Max((double) Bottom, rect.Bottom); + _height = Math.Max(maxBottom - top, 0); + } + + _x = left; + _y = top; + } + } + + /// + /// Union - Return the result of the union of rect1 and rect2. + /// + public static Rect Union(Rect rect1, Rect rect2) + { + rect1.Union(rect2); + return rect1; + } + + /// + /// Union - Update this rectangle to be the union of this and point. + /// + public void Union(Point point) + { + Union(new Rect(point, point)); + } + + /// + /// Union - Return the result of the union of rect and point. + /// + public static Rect Union(Rect rect, Point point) + { + rect.Union(new Rect(point, point)); + return rect; + } + + /// + /// Offset - translate the Location by the offset provided. + /// If this is Empty, this method is illegal. + /// + public void Offset(Vector offsetVector) + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Rect_CannotCallMethod); + } + + _x += offsetVector._x; + _y += offsetVector._y; + } + + /// + /// Offset - translate the Location by the offset provided + /// If this is Empty, this method is illegal. + /// + public void Offset(double offsetX, double offsetY) + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Rect_CannotCallMethod); + } + + _x += offsetX; + _y += offsetY; + } + + /// + /// Offset - return the result of offsetting rect by the offset provided + /// If this is Empty, this method is illegal. + /// + public static Rect Offset(Rect rect, Vector offsetVector) + { + rect.Offset(offsetVector.X, offsetVector.Y); + return rect; + } + + /// + /// Offset - return the result of offsetting rect by the offset provided + /// If this is Empty, this method is illegal. + /// + public static Rect Offset(Rect rect, double offsetX, double offsetY) + { + rect.Offset(offsetX, offsetY); + return rect; + } + + /// + /// Inflate - inflate the bounds by the size provided, in all directions + /// If this is Empty, this method is illegal. + /// + public void Inflate(Size size) + { + Inflate(size._width, size._height); + } + + /// + /// Inflate - inflate the bounds by the size provided, in all directions. + /// If -width is > Width / 2 or -height is > Height / 2, this Rect becomes Empty + /// If this is Empty, this method is illegal. + /// + public void Inflate(double width, double height) + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Rect_CannotCallMethod); + } + + _x -= width; + _y -= height; + + // Do two additions rather than multiplication by 2 to avoid spurious overflow + // That is: (A + 2 * B) != ((A + B) + B) if 2*B overflows. + // Note that multiplication by 2 might work in this case because A should start + // positive & be "clamped" to positive after, but consider A = Inf & B = -MAX. + _width += width; + _width += width; + _height += height; + _height += height; + + // We catch the case of inflation by less than -width/2 or -height/2 here. This also + // maintains the invariant that either the Rect is Empty or _width and _height are + // non-negative, even if the user parameters were NaN, though this isn't strictly maintained + // by other methods. + if (!(_width >= 0 && _height >= 0)) + { + this = s_empty; + } + } + + /// + /// Inflate - return the result of inflating rect by the size provided, in all directions + /// If this is Empty, this method is illegal. + /// + public static Rect Inflate(Rect rect, Size size) + { + rect.Inflate(size._width, size._height); + return rect; + } + + /// + /// Inflate - return the result of inflating rect by the size provided, in all directions + /// If this is Empty, this method is illegal. + /// + public static Rect Inflate(Rect rect, double width, double height) + { + rect.Inflate(width, height); + return rect; + } + + /// + /// Scale the rectangle in the X and Y directions + /// + /// The scale in X + /// The scale in Y + public void Scale(double scaleX, double scaleY) + { + if (IsEmpty) + { + return; + } + + _x *= scaleX; + _y *= scaleY; + _width *= scaleX; + _height *= scaleY; + + // If the scale in the X dimension is negative, we need to normalize X and Width + if (scaleX < 0) + { + // Make X the left-most edge again + _x += _width; + + // and make Width positive + _width *= -1; + } + + // Do the same for the Y dimension + if (scaleY < 0) + { + // Make Y the top-most edge again + _y += _height; + + // and make Height positive + _height *= -1; + } + } + + #endregion Public Methods + + #region Private Methods + + /// + /// ContainsInternal - Performs just the "point inside" logic + /// + /// + /// bool - true if the point is inside the rect + /// + /// The x-coord of the point to test + /// The y-coord of the point to test + private bool ContainsInternal(double x, double y) + { + // We include points on the edge as "contained". + // We do "x - _width <= _x" instead of "x <= _x + _width" + // so that this check works when _width is PositiveInfinity + // and _x is NegativeInfinity. + return ((x >= _x) && (x - _width <= _x) && + (y >= _y) && (y - _height <= _y)); + } + + private static Rect CreateEmptyRect() + { + Rect rect = new Rect + { + // We can't set these via the property setters because negatives widths + // are rejected in those APIs. + _x = Double.PositiveInfinity, + _y = Double.PositiveInfinity, + _width = Double.NegativeInfinity, + _height = Double.NegativeInfinity + }; + return rect; + } + + #endregion Private Methods + + #region Private Fields + + private static readonly Rect s_empty = CreateEmptyRect(); + + #endregion Private Fields + + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + + #region Public Methods + + + + + /// + /// Compares two Rect 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. + /// + /// + /// bool - true if the two Rect instances are exactly equal, false otherwise + /// + /// The first Rect to compare + /// The second Rect to compare + public static bool operator ==(Rect rect1, Rect rect2) + { + return rect1.X == rect2.X && + rect1.Y == rect2.Y && + rect1.Width == rect2.Width && + rect1.Height == rect2.Height; + } + + /// + /// Compares two Rect 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 Rect instances are exactly unequal, false otherwise + /// + /// The first Rect to compare + /// The second Rect to compare + public static bool operator !=(Rect rect1, Rect rect2) + { + return !(rect1 == rect2); + } + /// + /// Compares two Rect instances for object equality. 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 the two Rect instances are exactly equal, false otherwise + /// + /// The first Rect to compare + /// The second Rect to compare + public static bool Equals(Rect rect1, Rect rect2) + { + if (rect1.IsEmpty) + { + return rect2.IsEmpty; + } + else + { + return rect1.X.Equals(rect2.X) && + rect1.Y.Equals(rect2.Y) && + rect1.Width.Equals(rect2.Width) && + rect1.Height.Equals(rect2.Height); + } + } + + /// + /// Equals - compares this Rect 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 the object is an instance of Rect and if it's equal to "this". + /// + /// The object to compare to "this" + public override bool Equals(object o) + { + if ((null == o) || !(o is Rect)) + { + return false; + } + + Rect value = (Rect) o; + return Rect.Equals(this, value); + } + + /// + /// Equals - compares this Rect 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 Rect to compare to "this" + public bool Equals(Rect value) + { + return Rect.Equals(this, value); + } + /// + /// Returns the HashCode for this Rect + /// + /// + /// int - the HashCode for this Rect + /// + public override int GetHashCode() + { + if (IsEmpty) + { + return 0; + } + else + { + // Perform field-by-field XOR of HashCodes + return X.GetHashCode() ^ + Y.GetHashCode() ^ + Width.GetHashCode() ^ + Height.GetHashCode(); + } + } + + /// + /// Parse - returns an instance converted from the provided string using + /// the culture "en-US" + /// string with Rect data + /// + public static Rect Parse(string source) + { + throw new NotImplementedException(); + //IFormatProvider formatProvider = System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS; + + //TokenizerHelper th = new TokenizerHelper(source, formatProvider); + + //Rect value; + + //String firstToken = th.NextTokenRequired(); + + //// The token will already have had whitespace trimmed so we can do a + //// simple string compare. + //if (firstToken == "Empty") + //{ + // value = Empty; + //} + //else + //{ + // value = new Rect( + // Convert.ToDouble(firstToken, formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider)); + //} + + //// There should be no more tokens in this string. + //th.LastTokenRequired(); + + //return value; + } + + #endregion Public Methods + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + + + + #region Public Properties + + + + #endregion Public Properties + + //------------------------------------------------------ + // + // Protected Methods + // + //------------------------------------------------------ + + #region Protected Methods + + + + + + #endregion ProtectedMethods + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + #region Internal Methods + + + + + + + + + + #endregion Internal Methods + + //------------------------------------------------------ + // + // Internal Properties + // + //------------------------------------------------------ + + #region Internal Properties + + + /// + /// Creates a string representation of this object based on the current culture. + /// + /// + /// A string representation of this object. + /// + public override string ToString() + { + + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, null /* format provider */); + } + + /// + /// Creates a string representation of this object based on the IFormatProvider + /// passed in. If the provider is null, the CurrentCulture is used. + /// + /// + /// A string representation of this object. + /// + public string ToString(IFormatProvider provider) + { + + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, provider); + } + + ///// + ///// Creates a string representation of this object based on the format string + ///// and IFormatProvider passed in. + ///// If the provider is null, the CurrentCulture is used. + ///// See the documentation for IFormattable for more information. + ///// + ///// + ///// A string representation of this object. + ///// + //string IFormattable.ToString(string format, IFormatProvider provider) + //{ + + // // Delegate to the internal method which implements all ToString calls. + // return ConvertToString(format, provider); + //} + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + internal string ConvertToString(string format, IFormatProvider provider) + { + //if (IsEmpty) + //{ + // return "Empty"; + //} + + //// Helper to get the numeric list separator for a given culture. + //char separator = MS.Internal.TokenizerHelper.GetNumericListSeparator(provider); + //return String.Format(provider, + // "{1:" + format + "}{0}{2:" + format + "}{0}{3:" + format + "}{0}{4:" + format + "}", + // separator, + // _x, + // _y, + // _width, + // _height); + throw new NotImplementedException(); + } + + + + #endregion Internal Properties + + //------------------------------------------------------ + // + // Dependency Properties + // + //------------------------------------------------------ + + #region Dependency Properties + + + + #endregion Dependency Properties + + //------------------------------------------------------ + // + // Internal Fields + // + //------------------------------------------------------ + + #region Internal Fields + + + internal double _x; + internal double _y; + internal double _width; + internal double _height; + + + + + #endregion Internal Fields + + + + #region Constructors + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + + + + #endregion Constructors + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Size.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Size.cs new file mode 100644 index 0000000..626e8ae --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Size.cs @@ -0,0 +1,497 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace WpfInk.PresentationCore.System.Windows +{ + internal struct Size : IFormattable + { + #region Constructors + + /// + /// Constructor which sets the size's initial values. Width and Height must be non-negative + /// + /// double - The initial Width + /// double - THe initial Height + public Size(double width, double height) + { + if (width < 0 || height < 0) + { + throw new global::System.ArgumentException(SR.Size_WidthAndHeightCannotBeNegative); + } + + _width = width; + _height = height; + } + + #endregion Constructors + + #region Statics + + /// + /// Empty - a static property which provides an Empty size. Width and Height are + /// negative-infinity. This is the only situation + /// where size can be negative. + /// + public static Size Empty + { + get + { + return s_empty; + } + } + + #endregion Statics + + #region Public Methods and Properties + + /// + /// IsEmpty - this returns true if this size is the Empty size. + /// Note: If size is 0 this Size still contains a 0 or 1 dimensional set + /// of points, so this method should not be used to check for 0 area. + /// + public bool IsEmpty + { + get + { + return _width < 0; + } + } + + /// + /// Width - Default is 0, must be non-negative + /// + public double Width + { + get + { + return _width; + } + set + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Size_CannotModifyEmptySize); + } + + if (value < 0) + { + throw new global::System.ArgumentException(SR.Size_WidthCannotBeNegative); + } + + _width = value; + } + } + + /// + /// Height - Default is 0, must be non-negative. + /// + public double Height + { + get + { + return _height; + } + set + { + if (IsEmpty) + { + throw new global::System.InvalidOperationException(SR.Size_CannotModifyEmptySize); + } + + if (value < 0) + { + throw new global::System.ArgumentException(SR.Size_HeightCannotBeNegative); + } + + _height = value; + } + } + + #endregion Public Methods + + #region Public Operators + + /// + /// Explicit conversion to Vector. + /// + /// + /// Vector - A Vector equal to this Size + /// + /// Size - the Size to convert to a Vector + public static explicit operator Vector(Size size) + { + return new Vector(size._width, size._height); + } + + /// + /// Explicit conversion to Point + /// + /// + /// Point - A Point equal to this Size + /// + /// Size - the Size to convert to a Point + public static explicit operator Point(Size size) + { + return new Point(size._width, size._height); + } + + #endregion Public Operators + + #region Private Methods + + private static Size CreateEmptySize() + { + Size size = new Size + { + // We can't set these via the property setters because negatives widths + // are rejected in those APIs. + _width = Double.NegativeInfinity, + _height = Double.NegativeInfinity + }; + return size; + } + + #endregion Private Methods + + #region Private Fields + + private static readonly Size s_empty = CreateEmptySize(); + + #endregion Private Fields + + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + + #region Public Methods + + + + + /// + /// Compares two Size 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. + /// + /// + /// bool - true if the two Size instances are exactly equal, false otherwise + /// + /// The first Size to compare + /// The second Size to compare + public static bool operator ==(Size size1, Size size2) + { + return size1.Width == size2.Width && + size1.Height == size2.Height; + } + + /// + /// Compares two Size 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 Size instances are exactly unequal, false otherwise + /// + /// The first Size to compare + /// The second Size to compare + public static bool operator !=(Size size1, Size size2) + { + return !(size1 == size2); + } + /// + /// Compares two Size instances for object equality. 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 the two Size instances are exactly equal, false otherwise + /// + /// The first Size to compare + /// The second Size to compare + public static bool Equals(Size size1, Size size2) + { + if (size1.IsEmpty) + { + return size2.IsEmpty; + } + else + { + return size1.Width.Equals(size2.Width) && + size1.Height.Equals(size2.Height); + } + } + + /// + /// Equals - compares this Size 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 the object is an instance of Size and if it's equal to "this". + /// + /// The object to compare to "this" + public override bool Equals(object o) + { + if ((null == o) || !(o is Size)) + { + return false; + } + + Size value = (Size) o; + return Size.Equals(this, value); + } + + /// + /// Equals - compares this Size 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 Size to compare to "this" + public bool Equals(Size value) + { + return Size.Equals(this, value); + } + /// + /// Returns the HashCode for this Size + /// + /// + /// int - the HashCode for this Size + /// + public override int GetHashCode() + { + if (IsEmpty) + { + return 0; + } + else + { + // Perform field-by-field XOR of HashCodes + return Width.GetHashCode() ^ + Height.GetHashCode(); + } + } + + /// + /// Parse - returns an instance converted from the provided string using + /// the culture "en-US" + /// string with Size data + /// + public static Size Parse(string source) + { + throw new NotImplementedException(); + //IFormatProvider formatProvider = System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS; + + //TokenizerHelper th = new TokenizerHelper(source, formatProvider); + + //Size value; + + //String firstToken = th.NextTokenRequired(); + + //// The token will already have had whitespace trimmed so we can do a + //// simple string compare. + //if (firstToken == "Empty") + //{ + // value = Empty; + //} + //else + //{ + // value = new Size( + // Convert.ToDouble(firstToken, formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider)); + //} + + //// There should be no more tokens in this string. + //th.LastTokenRequired(); + + //return value; + } + + #endregion Public Methods + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + + + + #region Public Properties + + + + #endregion Public Properties + + //------------------------------------------------------ + // + // Protected Methods + // + //------------------------------------------------------ + + #region Protected Methods + + + + + + #endregion ProtectedMethods + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + #region Internal Methods + + + + + + + + + + #endregion Internal Methods + + //------------------------------------------------------ + // + // Internal Properties + // + //------------------------------------------------------ + + #region Internal Properties + + + /// + /// Creates a string representation of this object based on the current culture. + /// + /// + /// A string representation of this object. + /// + public override string ToString() + { + + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, null /* format provider */); + } + + /// + /// Creates a string representation of this object based on the IFormatProvider + /// passed in. If the provider is null, the CurrentCulture is used. + /// + /// + /// A string representation of this object. + /// + public string ToString(IFormatProvider provider) + { + + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + string IFormattable.ToString(string format, IFormatProvider provider) + { + + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(format, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + internal string ConvertToString(string format, IFormatProvider provider) + { + throw new NotImplementedException(); + //if (IsEmpty) + //{ + // return "Empty"; + //} + + //// Helper to get the numeric list separator for a given culture. + //char separator = MS.Internal.TokenizerHelper.GetNumericListSeparator(provider); + //return String.Format(provider, + // "{1:" + format + "}{0}{2:" + format + "}", + // separator, + // _width, + // _height); + } + + #endregion Internal Properties + + //------------------------------------------------------ + // + // Dependency Properties + // + //------------------------------------------------------ + + #region Dependency Properties + + + + #endregion Dependency Properties + + //------------------------------------------------------ + // + // Internal Fields + // + //------------------------------------------------------ + + #region Internal Fields + + + internal double _width; + internal double _height; + + + + + #endregion Internal Fields + + + + #region Constructors + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + + + + #endregion Constructors + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Vector.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Vector.cs new file mode 100644 index 0000000..5cd8ab7 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/PresentationCore/System/Windows/Vector.cs @@ -0,0 +1,557 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace WpfInk.PresentationCore.System.Windows +{ + internal struct Vector + { + #region Constructors + + /// + /// Constructor which sets the vector's initial values + /// + /// double - The initial X + /// double - THe initial Y + public Vector(double x, double y) + { + _x = x; + _y = y; + } + + #endregion Constructors + + #region Public Methods + + /// + /// Length Property - the length of this Vector + /// + public double Length + { + get + { + return Math.Sqrt(_x * _x + _y * _y); + } + } + + /// + /// LengthSquared Property - the squared length of this Vector + /// + public double LengthSquared + { + get + { + return _x * _x + _y * _y; + } + } + + /// + /// Normalize - Updates this Vector to maintain its direction, but to have a length + /// of 1. This is equivalent to dividing this Vector by Length + /// + public void Normalize() + { + // Avoid overflow + this /= Math.Max(Math.Abs(_x), Math.Abs(_y)); + this /= Length; + } + + /// + /// CrossProduct - Returns the cross product: vector1.X*vector2.Y - vector1.Y*vector2.X + /// + /// + /// Returns the cross product: vector1.X*vector2.Y - vector1.Y*vector2.X + /// + /// The first Vector + /// The second Vector + public static double CrossProduct(Vector vector1, Vector vector2) + { + return vector1._x * vector2._y - vector1._y * vector2._x; + } + + /// + /// AngleBetween - the angle between 2 vectors + /// + /// + /// Returns the the angle in degrees between vector1 and vector2 + /// + /// The first Vector + /// The second Vector + public static double AngleBetween(Vector vector1, Vector vector2) + { + double sin = vector1._x * vector2._y - vector2._x * vector1._y; + double cos = vector1._x * vector2._x + vector1._y * vector2._y; + + return Math.Atan2(sin, cos) * (180 / Math.PI); + } + + #endregion Public Methods + + #region Public Operators + /// + /// Operator -Vector (unary negation) + /// + public static Vector operator -(Vector vector) + { + return new Vector(-vector._x, -vector._y); + } + + /// + /// Negates the values of X and Y on this Vector + /// + public void Negate() + { + _x = -_x; + _y = -_y; + } + + /// + /// Operator Vector + Vector + /// + public static Vector operator +(Vector vector1, Vector vector2) + { + return new Vector(vector1._x + vector2._x, + vector1._y + vector2._y); + } + + /// + /// Add: Vector + Vector + /// + public static Vector Add(Vector vector1, Vector vector2) + { + return new Vector(vector1._x + vector2._x, + vector1._y + vector2._y); + } + + /// + /// Operator Vector - Vector + /// + public static Vector operator -(Vector vector1, Vector vector2) + { + return new Vector(vector1._x - vector2._x, + vector1._y - vector2._y); + } + + /// + /// Subtract: Vector - Vector + /// + public static Vector Subtract(Vector vector1, Vector vector2) + { + return new Vector(vector1._x - vector2._x, + vector1._y - vector2._y); + } + + /// + /// Operator Vector + Point + /// + public static Point operator +(Vector vector, Point point) + { + return new Point(point._x + vector._x, point._y + vector._y); + } + + /// + /// Add: Vector + Point + /// + public static Point Add(Vector vector, Point point) + { + return new Point(point._x + vector._x, point._y + vector._y); + } + + /// + /// Operator Vector * double + /// + public static Vector operator *(Vector vector, double scalar) + { + return new Vector(vector._x * scalar, + vector._y * scalar); + } + + /// + /// Multiply: Vector * double + /// + public static Vector Multiply(Vector vector, double scalar) + { + return new Vector(vector._x * scalar, + vector._y * scalar); + } + + /// + /// Operator double * Vector + /// + public static Vector operator *(double scalar, Vector vector) + { + return new Vector(vector._x * scalar, + vector._y * scalar); + } + + /// + /// Multiply: double * Vector + /// + public static Vector Multiply(double scalar, Vector vector) + { + return new Vector(vector._x * scalar, + vector._y * scalar); + } + + /// + /// Operator Vector / double + /// + public static Vector operator /(Vector vector, double scalar) + { + return vector * (1.0 / scalar); + } + + /// + /// Multiply: Vector / double + /// + public static Vector Divide(Vector vector, double scalar) + { + return vector * (1.0 / scalar); + } + + /// + /// Operator Vector * Vector, interpreted as their dot product + /// + public static double operator *(Vector vector1, Vector vector2) + { + return vector1._x * vector2._x + vector1._y * vector2._y; + } + + /// + /// Multiply - Returns the dot product: vector1.X*vector2.X + vector1.Y*vector2.Y + /// + /// + /// Returns the dot product: vector1.X*vector2.X + vector1.Y*vector2.Y + /// + /// The first Vector + /// The second Vector + public static double Multiply(Vector vector1, Vector vector2) + { + return vector1._x * vector2._x + vector1._y * vector2._y; + } + + /// + /// Determinant - Returns the determinant det(vector1, vector2) + /// + /// + /// Returns the determinant: vector1.X*vector2.Y - vector1.Y*vector2.X + /// + /// The first Vector + /// The second Vector + public static double Determinant(Vector vector1, Vector vector2) + { + return vector1._x * vector2._y - vector1._y * vector2._x; + } + + /// + /// Explicit conversion to Size. Note that since Size cannot contain negative values, + /// the resulting size will contains the absolute values of X and Y + /// + /// + /// Size - A Size equal to this Vector + /// + /// Vector - the Vector to convert to a Size + public static explicit operator Size(Vector vector) + { + return new Size(Math.Abs(vector._x), Math.Abs(vector._y)); + } + + /// + /// Explicit conversion to Point + /// + /// + /// Point - A Point equal to this Vector + /// + /// Vector - the Vector to convert to a Point + public static explicit operator Point(Vector vector) + { + return new Point(vector._x, vector._y); + } + #endregion Public Operators + + //------------------------------------------------------ + // + // Public Methods + // + //------------------------------------------------------ + + #region Public Methods + + + + + /// + /// Compares two Vector 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. + /// + /// + /// bool - true if the two Vector instances are exactly equal, false otherwise + /// + /// The first Vector to compare + /// The second Vector to compare + public static bool operator ==(Vector vector1, Vector vector2) + { + return vector1.X == vector2.X && + vector1.Y == vector2.Y; + } + + /// + /// Compares two Vector 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 Vector instances are exactly unequal, false otherwise + /// + /// The first Vector to compare + /// The second Vector to compare + public static bool operator !=(Vector vector1, Vector vector2) + { + return !(vector1 == vector2); + } + /// + /// Compares two Vector instances for object equality. 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 the two Vector instances are exactly equal, false otherwise + /// + /// The first Vector to compare + /// The second Vector to compare + public static bool Equals(Vector vector1, Vector vector2) + { + return vector1.X.Equals(vector2.X) && + vector1.Y.Equals(vector2.Y); + } + + /// + /// Equals - compares this Vector 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 the object is an instance of Vector and if it's equal to "this". + /// + /// The object to compare to "this" + public override bool Equals(object o) + { + if ((null == o) || !(o is Vector)) + { + return false; + } + + Vector value = (Vector) o; + return Vector.Equals(this, value); + } + + /// + /// Equals - compares this Vector 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 Vector to compare to "this" + public bool Equals(Vector value) + { + return Vector.Equals(this, value); + } + /// + /// Returns the HashCode for this Vector + /// + /// + /// int - the HashCode for this Vector + /// + public override int GetHashCode() + { + // Perform field-by-field XOR of HashCodes + return X.GetHashCode() ^ + Y.GetHashCode(); + } + + /// + /// Parse - returns an instance converted from the provided string using + /// the culture "en-US" + /// string with Vector data + /// + public static Vector Parse(string source) + { + throw new NotImplementedException(); + //IFormatProvider formatProvider = System.Windows.Markup.TypeConverterHelper.InvariantEnglishUS; + + //TokenizerHelper th = new TokenizerHelper(source, formatProvider); + + //Vector value; + + //String firstToken = th.NextTokenRequired(); + + //value = new Vector( + // Convert.ToDouble(firstToken, formatProvider), + // Convert.ToDouble(th.NextTokenRequired(), formatProvider)); + + //// There should be no more tokens in this string. + //th.LastTokenRequired(); + + //return value; + } + + #endregion Public Methods + + //------------------------------------------------------ + // + // Public Properties + // + //------------------------------------------------------ + + + + + #region Public Properties + + /// + /// X - double. Default value is 0. + /// + public double X + { + get + { + return _x; + } + + set + { + _x = value; + } + + } + + /// + /// Y - double. Default value is 0. + /// + public double Y + { + get + { + return _y; + } + + set + { + _y = value; + } + + } + + #endregion Public Properties + + //------------------------------------------------------ + // + // Protected Methods + // + //------------------------------------------------------ + + #region Protected Methods + + + + + + #endregion ProtectedMethods + + //------------------------------------------------------ + // + // Internal Methods + // + //------------------------------------------------------ + + #region Internal Methods + + + + + + + + + + #endregion Internal Methods + + //------------------------------------------------------ + // + // Internal Properties + // + //------------------------------------------------------ + + #region Internal Properties + + + /// + /// Creates a string representation of this object based on the current culture. + /// + /// + /// A string representation of this object. + /// + public override string ToString() + { + return $"({_x},{_y})"; + } + + #endregion Internal Properties + + //------------------------------------------------------ + // + // Dependency Properties + // + //------------------------------------------------------ + + #region Dependency Properties + + + + #endregion Dependency Properties + + //------------------------------------------------------ + // + // Internal Fields + // + //------------------------------------------------------ + + #region Internal Fields + + + internal double _x; + internal double _y; + + + + + #endregion Internal Fields + + + + #region Constructors + + //------------------------------------------------------ + // + // Constructors + // + //------------------------------------------------------ + + + + + #endregion Constructors + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/WindowsBase/Matrix.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/WindowsBase/Matrix.cs new file mode 100644 index 0000000..73d9560 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/WindowsBase/Matrix.cs @@ -0,0 +1,1029 @@ +// 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.ComponentModel; +using System.ComponentModel.Design.Serialization; +using System.Reflection; +using MS.Internal; +using MS.Internal.WindowsBase; +using System.Text; +using System.Collections; +using System.Globalization; +using System.Windows; +using System.Runtime.InteropServices; +using System.Security; +using WpfInk.PresentationCore.System.Windows; + +// IMPORTANT +// +// Rules for using matrix types. +// +// internal enum MatrixTypes +// { +// TRANSFORM_IS_IDENTITY = 0, +// TRANSFORM_IS_TRANSLATION = 1, +// TRANSFORM_IS_SCALING = 2, +// TRANSFORM_IS_UNKNOWN = 4 +// } +// +// 1. Matrix type must be one of 0, 1, 2, 4, or 3 (for scale and translation) +// 2. Matrix types are true but not exact! (E.G. A scale or identity transform could be marked as unknown or scale+translate.) +// 3. Therefore read-only operations can ignore the type with one exception +// EXCEPTION: A matrix tagged identity might have any coefficients instead of 1,0,0,1,0,0 +// This is the (now) classic no default constructor for structs issue +// 4. Matrix._type must be maintained by mutation operations +// 5. MS.Internal.MatrixUtil uses unsafe code to access the private members of Matrix including _type. +// +// In Jan 2005 the matrix types were changed from being EXACT (i.e. a +// scale matrix is always tagged as a scale and not something more +// general.) This resulted in about a 2% speed up in matrix +// multiplication. +// +// The special cases for matrix multiplication speed up scale*scale +// and translation*translation by 30% compared to a single "no-branch" +// multiplication algorithm. Matrix multiplication of two unknown +// matrices is slowed by 20% compared to the no-branch algorithm. +// +// windows/wcp/DevTest/Drts/MediaApi/MediaPerf.cs includes the +// simple test of matrix multiplication speed used for these results. + +namespace WpfInk.WindowsBase.System.Windows.Media +{ + /// + /// Matrix + /// + internal partial struct Matrix : IFormattable + { + // the transform is identity by default + // Actually fill in the fields - some (internal) code uses the fields directly for perf. + private static Matrix s_identity = CreateIdentity(); + + #region Constructor + + /// + /// Creates a matrix of the form + /// / m11, m12, 0 \ + /// | m21, m22, 0 | + /// \ offsetX, offsetY, 1 / + /// + public Matrix(double m11, double m12, + double m21, double m22, + double offsetX, double offsetY) + { + this._m11 = m11; + this._m12 = m12; + this._m21 = m21; + this._m22 = m22; + this._offsetX = offsetX; + this._offsetY = offsetY; + _type = MatrixTypes.TRANSFORM_IS_UNKNOWN; + _padding = 0; + + // We will detect EXACT identity, scale, translation or + // scale+translation and use special case algorithms. + DeriveMatrixType(); + } + + #endregion Constructor + + #region Identity + + /// + /// Identity + /// + public static Matrix Identity + { + get + { + return s_identity; + } + } + + /// + /// Sets the matrix to identity. + /// + public void SetIdentity() + { + _type = MatrixTypes.TRANSFORM_IS_IDENTITY; + } + + /// + /// Tests whether or not a given transform is an identity transform + /// + public bool IsIdentity + { + get + { + return (_type == MatrixTypes.TRANSFORM_IS_IDENTITY || + (_m11 == 1 && _m12 == 0 && _m21 == 0 && _m22 == 1 && _offsetX == 0 && _offsetY == 0)); + } + } + + #endregion Identity + + #region Operators + /// + /// Multiplies two transformations. + /// + public static Matrix operator *(Matrix trans1, Matrix trans2) + { + MatrixUtil.MultiplyMatrix(ref trans1, ref trans2); + trans1.Debug_CheckType(); + return trans1; + } + + /// + /// Multiply + /// + public static Matrix Multiply(Matrix trans1, Matrix trans2) + { + MatrixUtil.MultiplyMatrix(ref trans1, ref trans2); + trans1.Debug_CheckType(); + return trans1; + } + + #endregion Operators + + #region Combine Methods + + /// + /// Append - "this" becomes this * matrix, the same as this *= matrix. + /// + /// The Matrix to append to this Matrix + public void Append(Matrix matrix) + { + this *= matrix; + } + + /// + /// Prepend - "this" becomes matrix * this, the same as this = matrix * this. + /// + /// The Matrix to prepend to this Matrix + public void Prepend(Matrix matrix) + { + this = matrix * this; + } + + /// + /// Rotates this matrix about the origin + /// + /// The angle to rotate specified in degrees + public void Rotate(double angle) + { + angle %= 360.0; // Doing the modulo before converting to radians reduces total error + this *= CreateRotationRadians(angle * (Math.PI / 180.0)); + } + + /// + /// Prepends a rotation about the origin to "this" + /// + /// The angle to rotate specified in degrees + public void RotatePrepend(double angle) + { + angle %= 360.0; // Doing the modulo before converting to radians reduces total error + this = CreateRotationRadians(angle * (Math.PI / 180.0)) * this; + } + + /// + /// Rotates this matrix about the given point + /// + /// The angle to rotate specified in degrees + /// The centerX of rotation + /// The centerY of rotation + public void RotateAt(double angle, double centerX, double centerY) + { + angle %= 360.0; // Doing the modulo before converting to radians reduces total error + this *= CreateRotationRadians(angle * (Math.PI / 180.0), centerX, centerY); + } + + /// + /// Prepends a rotation about the given point to "this" + /// + /// The angle to rotate specified in degrees + /// The centerX of rotation + /// The centerY of rotation + public void RotateAtPrepend(double angle, double centerX, double centerY) + { + angle %= 360.0; // Doing the modulo before converting to radians reduces total error + this = CreateRotationRadians(angle * (Math.PI / 180.0), centerX, centerY) * this; + } + + /// + /// Scales this matrix around the origin + /// + /// The scale factor in the x dimension + /// The scale factor in the y dimension + public void Scale(double scaleX, double scaleY) + { + this *= CreateScaling(scaleX, scaleY); + } + + /// + /// Prepends a scale around the origin to "this" + /// + /// The scale factor in the x dimension + /// The scale factor in the y dimension + public void ScalePrepend(double scaleX, double scaleY) + { + this = CreateScaling(scaleX, scaleY) * this; + } + + /// + /// Scales this matrix around the center provided + /// + /// The scale factor in the x dimension + /// The scale factor in the y dimension + /// The centerX about which to scale + /// The centerY about which to scale + public void ScaleAt(double scaleX, double scaleY, double centerX, double centerY) + { + this *= CreateScaling(scaleX, scaleY, centerX, centerY); + } + + /// + /// Prepends a scale around the center provided to "this" + /// + /// The scale factor in the x dimension + /// The scale factor in the y dimension + /// The centerX about which to scale + /// The centerY about which to scale + public void ScaleAtPrepend(double scaleX, double scaleY, double centerX, double centerY) + { + this = CreateScaling(scaleX, scaleY, centerX, centerY) * this; + } + + /// + /// Skews this matrix + /// + /// The skew angle in the x dimension in degrees + /// The skew angle in the y dimension in degrees + public void Skew(double skewX, double skewY) + { + skewX %= 360; + skewY %= 360; + this *= CreateSkewRadians(skewX * (Math.PI / 180.0), + skewY * (Math.PI / 180.0)); + } + + /// + /// Prepends a skew to this matrix + /// + /// The skew angle in the x dimension in degrees + /// The skew angle in the y dimension in degrees + public void SkewPrepend(double skewX, double skewY) + { + skewX %= 360; + skewY %= 360; + this = CreateSkewRadians(skewX * (Math.PI / 180.0), + skewY * (Math.PI / 180.0)) * this; + } + + /// + /// Translates this matrix + /// + /// The offset in the x dimension + /// The offset in the y dimension + public void Translate(double offsetX, double offsetY) + { + // + // / a b 0 \ / 1 0 0 \ / a b 0 \ + // | c d 0 | * | 0 1 0 | = | c d 0 | + // \ e f 1 / \ x y 1 / \ e+x f+y 1 / + // + // (where e = _offsetX and f == _offsetY) + // + + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + // Values would be incorrect if matrix was created using default constructor. + // or if SetIdentity was called on a matrix which had values. + // + SetMatrix(1, 0, + 0, 1, + offsetX, offsetY, + MatrixTypes.TRANSFORM_IS_TRANSLATION); + } + else if (_type == MatrixTypes.TRANSFORM_IS_UNKNOWN) + { + _offsetX += offsetX; + _offsetY += offsetY; + } + else + { + _offsetX += offsetX; + _offsetY += offsetY; + + // If matrix wasn't unknown we added a translation + _type |= MatrixTypes.TRANSFORM_IS_TRANSLATION; + } + + Debug_CheckType(); + } + + /// + /// Prepends a translation to this matrix + /// + /// The offset in the x dimension + /// The offset in the y dimension + public void TranslatePrepend(double offsetX, double offsetY) + { + this = CreateTranslation(offsetX, offsetY) * this; + } + + #endregion Set Methods + + #region Transformation Services + + /// + /// Transform - returns the result of transforming the point by this matrix + /// + /// + /// The transformed point + /// + /// The Point to transform + public Point Transform(Point point) + { + Point newPoint = point; + var x = newPoint.X; + var y = newPoint.Y; + MultiplyPoint(ref x, ref y); + return new Point(x, y); + } + + /// + /// Transform - Transforms each point in the array by this matrix + /// + /// The Point array to transform + public void Transform(Point[] points) + { + if (points != null) + { + for (int i = 0; i < points.Length; i++) + { + var point = points[i]; + var x = point.X; + var y = point.Y; + MultiplyPoint(ref x, ref y); + points[i] = new Point(x, y); + } + } + } + + /// + /// Transform - returns the result of transforming the Vector by this matrix. + /// + /// + /// The transformed vector + /// + /// The Vector to transform + public Vector Transform(Vector vector) + { + var x = vector.X; + var y = vector.Y; + MultiplyVector(ref x, ref y); + Vector newVector = new Vector(x, y); + return newVector; + } + + /// + /// Transform - Transforms each Vector in the array by this matrix. + /// + /// The Vector array to transform + public void Transform(Vector[] vectors) + { + if (vectors != null) + { + for (int i = 0; i < vectors.Length; i++) + { + var vector = vectors[i]; + var x = vector.X; + var y = vector.Y; + MultiplyVector(ref x, ref y); + Vector newVector = new Vector(x, y); + vectors[i] = newVector; + } + } + } + + #endregion Transformation Services + + #region Inversion + + /// + /// The determinant of this matrix + /// + public double Determinant + { + get + { + switch (_type) + { + case MatrixTypes.TRANSFORM_IS_IDENTITY: + case MatrixTypes.TRANSFORM_IS_TRANSLATION: + return 1.0; + case MatrixTypes.TRANSFORM_IS_SCALING: + case MatrixTypes.TRANSFORM_IS_SCALING | MatrixTypes.TRANSFORM_IS_TRANSLATION: + return (_m11 * _m22); + default: + return (_m11 * _m22) - (_m12 * _m21); + } + } + } + + /// + /// HasInverse Property - returns true if this matrix is invertable, false otherwise. + /// + public bool HasInverse + { + get + { + return !DoubleUtil.IsZero(Determinant); + } + } + + /// + /// Replaces matrix with the inverse of the transformation. This will throw an InvalidOperationException + /// if !HasInverse + /// + /// + /// This will throw an InvalidOperationException if the matrix is non-invertable + /// + public void Invert() + { + double determinant = Determinant; + + if (DoubleUtil.IsZero(determinant)) + { + throw new global::System.InvalidOperationException(); + } + + // Inversion does not change the type of a matrix. + switch (_type) + { + case MatrixTypes.TRANSFORM_IS_IDENTITY: + break; + case MatrixTypes.TRANSFORM_IS_SCALING: + { + _m11 = 1.0 / _m11; + _m22 = 1.0 / _m22; + } + break; + case MatrixTypes.TRANSFORM_IS_TRANSLATION: + _offsetX = -_offsetX; + _offsetY = -_offsetY; + break; + case MatrixTypes.TRANSFORM_IS_SCALING | MatrixTypes.TRANSFORM_IS_TRANSLATION: + { + _m11 = 1.0 / _m11; + _m22 = 1.0 / _m22; + _offsetX = -_offsetX * _m11; + _offsetY = -_offsetY * _m22; + } + break; + default: + { + double invdet = 1.0 / determinant; + SetMatrix(_m22 * invdet, + -_m12 * invdet, + -_m21 * invdet, + _m11 * invdet, + (_m21 * _offsetY - _offsetX * _m22) * invdet, + (_offsetX * _m12 - _m11 * _offsetY) * invdet, + MatrixTypes.TRANSFORM_IS_UNKNOWN); + } + break; + } + } + + #endregion Inversion + + #region Public Properties + + /// + /// M11 + /// + public double M11 + { + get + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + return 1.0; + } + else + { + return _m11; + } + } + set + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + SetMatrix(value, 0, + 0, 1, + 0, 0, + MatrixTypes.TRANSFORM_IS_SCALING); + } + else + { + _m11 = value; + if (_type != MatrixTypes.TRANSFORM_IS_UNKNOWN) + { + _type |= MatrixTypes.TRANSFORM_IS_SCALING; + } + } + } + } + + /// + /// M12 + /// + public double M12 + { + get + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + return 0; + } + else + { + return _m12; + } + } + set + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + SetMatrix(1, value, + 0, 1, + 0, 0, + MatrixTypes.TRANSFORM_IS_UNKNOWN); + } + else + { + _m12 = value; + _type = MatrixTypes.TRANSFORM_IS_UNKNOWN; + } + } + } + + /// + /// M22 + /// + public double M21 + { + get + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + return 0; + } + else + { + return _m21; + } + } + set + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + SetMatrix(1, 0, + value, 1, + 0, 0, + MatrixTypes.TRANSFORM_IS_UNKNOWN); + } + else + { + _m21 = value; + _type = MatrixTypes.TRANSFORM_IS_UNKNOWN; + } + } + } + + /// + /// M22 + /// + public double M22 + { + get + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + return 1.0; + } + else + { + return _m22; + } + } + set + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + SetMatrix(1, 0, + 0, value, + 0, 0, + MatrixTypes.TRANSFORM_IS_SCALING); + } + else + { + _m22 = value; + if (_type != MatrixTypes.TRANSFORM_IS_UNKNOWN) + { + _type |= MatrixTypes.TRANSFORM_IS_SCALING; + } + } + } + } + + /// + /// OffsetX + /// + public double OffsetX + { + get + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + return 0; + } + else + { + return _offsetX; + } + } + set + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + SetMatrix(1, 0, + 0, 1, + value, 0, + MatrixTypes.TRANSFORM_IS_TRANSLATION); + } + else + { + _offsetX = value; + if (_type != MatrixTypes.TRANSFORM_IS_UNKNOWN) + { + _type |= MatrixTypes.TRANSFORM_IS_TRANSLATION; + } + } + } + } + + /// + /// OffsetY + /// + public double OffsetY + { + get + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + return 0; + } + else + { + return _offsetY; + } + } + set + { + if (_type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + SetMatrix(1, 0, + 0, 1, + 0, value, + MatrixTypes.TRANSFORM_IS_TRANSLATION); + } + else + { + _offsetY = value; + if (_type != MatrixTypes.TRANSFORM_IS_UNKNOWN) + { + _type |= MatrixTypes.TRANSFORM_IS_TRANSLATION; + } + } + } + } + + #endregion Public Properties + + #region Internal Methods + /// + /// MultiplyVector + /// + internal void MultiplyVector(ref double x, ref double y) + { + switch (_type) + { + case MatrixTypes.TRANSFORM_IS_IDENTITY: + case MatrixTypes.TRANSFORM_IS_TRANSLATION: + return; + case MatrixTypes.TRANSFORM_IS_SCALING: + case MatrixTypes.TRANSFORM_IS_SCALING | MatrixTypes.TRANSFORM_IS_TRANSLATION: + x *= _m11; + y *= _m22; + break; + default: + double xadd = y * _m21; + double yadd = x * _m12; + x *= _m11; + x += xadd; + y *= _m22; + y += yadd; + break; + } + } + + /// + /// MultiplyPoint + /// + internal void MultiplyPoint(ref double x, ref double y) + { + switch (_type) + { + case MatrixTypes.TRANSFORM_IS_IDENTITY: + return; + case MatrixTypes.TRANSFORM_IS_TRANSLATION: + x += _offsetX; + y += _offsetY; + return; + case MatrixTypes.TRANSFORM_IS_SCALING: + x *= _m11; + y *= _m22; + return; + case MatrixTypes.TRANSFORM_IS_SCALING | MatrixTypes.TRANSFORM_IS_TRANSLATION: + x *= _m11; + x += _offsetX; + y *= _m22; + y += _offsetY; + break; + default: + double xadd = y * _m21 + _offsetX; + double yadd = x * _m12 + _offsetY; + x *= _m11; + x += xadd; + y *= _m22; + y += yadd; + break; + } + } + + /// + /// Creates a rotation transformation about the given point + /// + /// The angle to rotate specified in radians + internal static Matrix CreateRotationRadians(double angle) + { + return CreateRotationRadians(angle, /* centerX = */ 0, /* centerY = */ 0); + } + + /// + /// Creates a rotation transformation about the given point + /// + /// The angle to rotate specified in radians + /// The centerX of rotation + /// The centerY of rotation + internal static Matrix CreateRotationRadians(double angle, double centerX, double centerY) + { + Matrix matrix = new Matrix(); + + double sin = Math.Sin(angle); + double cos = Math.Cos(angle); + double dx = (centerX * (1.0 - cos)) + (centerY * sin); + double dy = (centerY * (1.0 - cos)) - (centerX * sin); + + matrix.SetMatrix(cos, sin, + -sin, cos, + dx, dy, + MatrixTypes.TRANSFORM_IS_UNKNOWN); + + return matrix; + } + + /// + /// Creates a scaling transform around the given point + /// + /// The scale factor in the x dimension + /// The scale factor in the y dimension + /// The centerX of scaling + /// The centerY of scaling + internal static Matrix CreateScaling(double scaleX, double scaleY, double centerX, double centerY) + { + Matrix matrix = new Matrix(); + + matrix.SetMatrix(scaleX, 0, + 0, scaleY, + centerX - scaleX * centerX, centerY - scaleY * centerY, + MatrixTypes.TRANSFORM_IS_SCALING | MatrixTypes.TRANSFORM_IS_TRANSLATION); + + return matrix; + } + + /// + /// Creates a scaling transform around the origin + /// + /// The scale factor in the x dimension + /// The scale factor in the y dimension + internal static Matrix CreateScaling(double scaleX, double scaleY) + { + Matrix matrix = new Matrix(); + matrix.SetMatrix(scaleX, 0, + 0, scaleY, + 0, 0, + MatrixTypes.TRANSFORM_IS_SCALING); + return matrix; + } + + /// + /// Creates a skew transform + /// + /// The skew angle in the x dimension in degrees + /// The skew angle in the y dimension in degrees + internal static Matrix CreateSkewRadians(double skewX, double skewY) + { + Matrix matrix = new Matrix(); + + matrix.SetMatrix(1.0, Math.Tan(skewY), + Math.Tan(skewX), 1.0, + 0.0, 0.0, + MatrixTypes.TRANSFORM_IS_UNKNOWN); + + return matrix; + } + + /// + /// Sets the transformation to the given translation specified by the offset vector. + /// + /// The offset in X + /// The offset in Y + internal static Matrix CreateTranslation(double offsetX, double offsetY) + { + Matrix matrix = new Matrix(); + + matrix.SetMatrix(1, 0, + 0, 1, + offsetX, offsetY, + MatrixTypes.TRANSFORM_IS_TRANSLATION); + + return matrix; + } + + #endregion Internal Methods + + #region Private Methods + /// + /// Sets the transformation to the identity. + /// + private static Matrix CreateIdentity() + { + Matrix matrix = new Matrix(); + matrix.SetMatrix(1, 0, + 0, 1, + 0, 0, + MatrixTypes.TRANSFORM_IS_IDENTITY); + return matrix; + } + + /// + /// Sets the transform to + /// / m11, m12, 0 \ + /// | m21, m22, 0 | + /// \ offsetX, offsetY, 1 / + /// where offsetX, offsetY is the translation. + /// + private void SetMatrix(double m11, double m12, + double m21, double m22, + double offsetX, double offsetY, + MatrixTypes type) + { + this._m11 = m11; + this._m12 = m12; + this._m21 = m21; + this._m22 = m22; + this._offsetX = offsetX; + this._offsetY = offsetY; + this._type = type; + } + + /// + /// Set the type of the matrix based on its current contents + /// + private void DeriveMatrixType() + { + _type = 0; + + // Now classify our matrix. + if (!(_m21 == 0 && _m12 == 0)) + { + _type = MatrixTypes.TRANSFORM_IS_UNKNOWN; + return; + } + + if (!(_m11 == 1 && _m22 == 1)) + { + _type = MatrixTypes.TRANSFORM_IS_SCALING; + } + + if (!(_offsetX == 0 && _offsetY == 0)) + { + _type |= MatrixTypes.TRANSFORM_IS_TRANSLATION; + } + + if (0 == (_type & (MatrixTypes.TRANSFORM_IS_TRANSLATION | MatrixTypes.TRANSFORM_IS_SCALING))) + { + // We have an identity matrix. + _type = MatrixTypes.TRANSFORM_IS_IDENTITY; + } + return; + } + + /// + /// Asserts that the matrix tag is one of the valid options and + /// that coefficients are correct. + /// + [Conditional("DEBUG")] + private void Debug_CheckType() + { + switch (_type) + { + case MatrixTypes.TRANSFORM_IS_IDENTITY: + return; + case MatrixTypes.TRANSFORM_IS_UNKNOWN: + return; + case MatrixTypes.TRANSFORM_IS_SCALING: + Debug.Assert(_m21 == 0); + Debug.Assert(_m12 == 0); + Debug.Assert(_offsetX == 0); + Debug.Assert(_offsetY == 0); + return; + case MatrixTypes.TRANSFORM_IS_TRANSLATION: + Debug.Assert(_m21 == 0); + Debug.Assert(_m12 == 0); + Debug.Assert(_m11 == 1); + Debug.Assert(_m22 == 1); + return; + case MatrixTypes.TRANSFORM_IS_SCALING | MatrixTypes.TRANSFORM_IS_TRANSLATION: + Debug.Assert(_m21 == 0); + Debug.Assert(_m12 == 0); + return; + default: + Debug.Assert(false); + return; + } + } + + #endregion Private Methods + + #region Private Properties and Fields + + /// + /// Efficient but conservative test for identity. Returns + /// true if the the matrix is identity. If it returns false + /// the matrix may still be identity. + /// + private bool IsDistinguishedIdentity + { + get + { + return _type == MatrixTypes.TRANSFORM_IS_IDENTITY; + } + } + + // The hash code for a matrix is the xor of its element's hashes. + // Since the identity matrix has 2 1's and 4 0's its hash is 0. + private const int c_identityHashCode = 0; + + #endregion Private Properties and Fields + + internal double _m11; + internal double _m12; + internal double _m21; + internal double _m22; + internal double _offsetX; + internal double _offsetY; + internal MatrixTypes _type; + + // This field is only used by unmanaged code which isn't detected by the compiler. +#pragma warning disable 0414 + // Matrix in blt'd to unmanaged code, so this is padding + // to align structure. + // + // Testing note: Validate that this blt will work on 64-bit + // + internal Int32 _padding; +#pragma warning restore 0414 + public string ToString(string? format, IFormatProvider? formatProvider) + { + return ""; + } + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/WindowsBase/MatrixUtil.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/WindowsBase/MatrixUtil.cs new file mode 100644 index 0000000..20d7406 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/WindowsBase/MatrixUtil.cs @@ -0,0 +1,212 @@ +// 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. + +// +// +// +// Description: This file contains the implementation of MatrixUtil, which +// provides matrix multiply code. +// +// +// +// + +using System; +using System.Windows; +using System.Diagnostics; +using System.Security; +using WpfInk.WindowsBase.System.Windows.Media; +#if WINDOWS_BASE + using MS.Internal.WindowsBase; +#elif PRESENTATION_CORE + using MS.Internal.PresentationCore; +#elif PRESENTATIONFRAMEWORK + using MS.Internal.PresentationFramework; +#elif DRT + using MS.Internal.Drt; +#else +//#error Attempt to use FriendAccessAllowedAttribute from an unknown assembly. +#endif + +namespace MS.Internal +{ + // MatrixTypes + [System.Flags] + internal enum MatrixTypes + { + TRANSFORM_IS_IDENTITY = 0, + TRANSFORM_IS_TRANSLATION = 1, + TRANSFORM_IS_SCALING = 2, + TRANSFORM_IS_UNKNOWN = 4 + } + + internal static class MatrixUtil + { + /// + /// Multiplies two transformations, where the behavior is matrix1 *= matrix2. + /// This code exists so that we can efficient combine matrices without copying + /// the data around, since each matrix is 52 bytes. + /// To reduce duplication and to ensure consistent behavior, this is the + /// method which is used to implement Matrix * Matrix as well. + /// + internal static void MultiplyMatrix(ref Matrix matrix1, ref Matrix matrix2) + { + MatrixTypes type1 = matrix1._type; + MatrixTypes type2 = matrix2._type; + + // Check for idents + + // If the second is ident, we can just return + if (type2 == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + return; + } + + // If the first is ident, we can just copy the memory across. + if (type1 == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + matrix1 = matrix2; + return; + } + + // Optimize for translate case, where the second is a translate + if (type2 == MatrixTypes.TRANSFORM_IS_TRANSLATION) + { + // 2 additions + matrix1._offsetX += matrix2._offsetX; + matrix1._offsetY += matrix2._offsetY; + + // If matrix 1 wasn't unknown we added a translation + if (type1 != MatrixTypes.TRANSFORM_IS_UNKNOWN) + { + matrix1._type |= MatrixTypes.TRANSFORM_IS_TRANSLATION; + } + + return; + } + + // Check for the first value being a translate + if (type1 == MatrixTypes.TRANSFORM_IS_TRANSLATION) + { + // Save off the old offsets + double offsetX = matrix1._offsetX; + double offsetY = matrix1._offsetY; + + // Copy the matrix + matrix1 = matrix2; + + matrix1._offsetX = offsetX * matrix2._m11 + offsetY * matrix2._m21 + matrix2._offsetX; + matrix1._offsetY = offsetX * matrix2._m12 + offsetY * matrix2._m22 + matrix2._offsetY; + + if (type2 == MatrixTypes.TRANSFORM_IS_UNKNOWN) + { + matrix1._type = MatrixTypes.TRANSFORM_IS_UNKNOWN; + } + else + { + matrix1._type = MatrixTypes.TRANSFORM_IS_SCALING | MatrixTypes.TRANSFORM_IS_TRANSLATION; + } + return; + } + + // The following code combines the type of the transformations so that the high nibble + // is "this"'s type, and the low nibble is mat's type. This allows for a switch rather + // than nested switches. + + // trans1._type | trans2._type + // 7 6 5 4 | 3 2 1 0 + int combinedType = ((int) type1 << 4) | (int) type2; + + switch (combinedType) + { + case 34: // S * S + // 2 multiplications + matrix1._m11 *= matrix2._m11; + matrix1._m22 *= matrix2._m22; + return; + + case 35: // S * S|T + matrix1._m11 *= matrix2._m11; + matrix1._m22 *= matrix2._m22; + matrix1._offsetX = matrix2._offsetX; + matrix1._offsetY = matrix2._offsetY; + + // Transform set to Translate and Scale + matrix1._type = MatrixTypes.TRANSFORM_IS_TRANSLATION | MatrixTypes.TRANSFORM_IS_SCALING; + return; + + case 50: // S|T * S + matrix1._m11 *= matrix2._m11; + matrix1._m22 *= matrix2._m22; + matrix1._offsetX *= matrix2._m11; + matrix1._offsetY *= matrix2._m22; + return; + + case 51: // S|T * S|T + matrix1._m11 *= matrix2._m11; + matrix1._m22 *= matrix2._m22; + matrix1._offsetX = matrix2._m11 * matrix1._offsetX + matrix2._offsetX; + matrix1._offsetY = matrix2._m22 * matrix1._offsetY + matrix2._offsetY; + return; + case 36: // S * U + case 52: // S|T * U + case 66: // U * S + case 67: // U * S|T + case 68: // U * U + matrix1 = new Matrix( + matrix1._m11 * matrix2._m11 + matrix1._m12 * matrix2._m21, + matrix1._m11 * matrix2._m12 + matrix1._m12 * matrix2._m22, + + matrix1._m21 * matrix2._m11 + matrix1._m22 * matrix2._m21, + matrix1._m21 * matrix2._m12 + matrix1._m22 * matrix2._m22, + + matrix1._offsetX * matrix2._m11 + matrix1._offsetY * matrix2._m21 + matrix2._offsetX, + matrix1._offsetX * matrix2._m12 + matrix1._offsetY * matrix2._m22 + matrix2._offsetY); + return; +#if DEBUG + default: + Debug.Fail("Matrix multiply hit an invalid case: " + combinedType); + break; +#endif + } + } + + /// + /// Applies an offset to the specified matrix in place. + /// + internal static void PrependOffset( + ref Matrix matrix, + double offsetX, + double offsetY) + { + if (matrix._type == MatrixTypes.TRANSFORM_IS_IDENTITY) + { + matrix = new Matrix(1, 0, 0, 1, offsetX, offsetY); + matrix._type = MatrixTypes.TRANSFORM_IS_TRANSLATION; + } + else + { + // + // / 1 0 0 \ / m11 m12 0 \ + // | 0 1 0 | * | m21 m22 0 | + // \ tx ty 1 / \ ox oy 1 / + // + // / m11 m12 0 \ + // = | m21 m22 0 | + // \ m11*tx+m21*ty+ox m12*tx + m22*ty + oy 1 / + // + + matrix._offsetX += matrix._m11 * offsetX + matrix._m21 * offsetY; + matrix._offsetY += matrix._m12 * offsetX + matrix._m22 * offsetY; + + // It just gained a translate if was a scale transform. Identity transform is handled above. + Debug.Assert(matrix._type != MatrixTypes.TRANSFORM_IS_IDENTITY); + if (matrix._type != MatrixTypes.TRANSFORM_IS_UNKNOWN) + { + matrix._type |= MatrixTypes.TRANSFORM_IS_TRANSLATION; + } + } + } + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/DoubleUtil.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/DoubleUtil.cs new file mode 100644 index 0000000..a5c64f2 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/DoubleUtil.cs @@ -0,0 +1,270 @@ +// 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. + +// +// +// +// Description: This file contains the implementation of DoubleUtil, which +// provides "fuzzy" comparison functionality for doubles and +// double-based classes and structs in our code. +// +// +// +// +// + +using System; +using System.Windows; +using System.Runtime.InteropServices; +using WpfInk.PresentationCore.System.Windows; + +#if WINDOWS_BASE + using MS.Internal.WindowsBase; +#elif PRESENTATION_CORE + using MS.Internal.PresentationCore; +#elif PRESENTATIONFRAMEWORK + using MS.Internal.PresentationFramework; +#elif DRT + using MS.Internal.Drt; +#else +//#error Attempt to use FriendAccessAllowedAttribute from an unknown assembly. +#endif + +namespace MS.Internal +{ + internal static class DoubleUtil + { + // Const values come from sdk\inc\crt\float.h + internal const double DBL_EPSILON = 2.2204460492503131e-016; /* smallest such that 1.0+DBL_EPSILON != 1.0 */ + internal const float FLT_MIN = 1.175494351e-38F; /* Number close to zero, where float.MinValue is -float.MaxValue */ + + /// + /// AreClose - Returns whether or not two doubles are "close". That is, whether or + /// not they are within epsilon of each other. Note that this epsilon is proportional + /// to the numbers themselves to that AreClose survives scalar multiplication. + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be + /// used for optimizations *only*. + /// + /// + /// bool - the result of the AreClose comparision. + /// + /// The first double to compare. + /// The second double to compare. + public static bool AreClose(double value1, double value2) + { + //in case they are Infinities (then epsilon check does not work) + if (value1 == value2) return true; + // This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON + double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON; + double delta = value1 - value2; + return (-eps < delta) && (eps > delta); + } + + /// + /// LessThan - Returns whether or not the first double is less than the second double. + /// That is, whether or not the first is strictly less than *and* not within epsilon of + /// the other number. Note that this epsilon is proportional to the numbers themselves + /// to that AreClose survives scalar multiplication. Note, + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be + /// used for optimizations *only*. + /// + /// + /// bool - the result of the LessThan comparision. + /// + /// The first double to compare. + /// The second double to compare. + public static bool LessThan(double value1, double value2) + { + return (value1 < value2) && !AreClose(value1, value2); + } + + + /// + /// GreaterThan - Returns whether or not the first double is greater than the second double. + /// That is, whether or not the first is strictly greater than *and* not within epsilon of + /// the other number. Note that this epsilon is proportional to the numbers themselves + /// to that AreClose survives scalar multiplication. Note, + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be + /// used for optimizations *only*. + /// + /// + /// bool - the result of the GreaterThan comparision. + /// + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThan(double value1, double value2) + { + return (value1 > value2) && !AreClose(value1, value2); + } + + /// + /// LessThanOrClose - Returns whether or not the first double is less than or close to + /// the second double. That is, whether or not the first is strictly less than or within + /// epsilon of the other number. Note that this epsilon is proportional to the numbers + /// themselves to that AreClose survives scalar multiplication. Note, + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be + /// used for optimizations *only*. + /// + /// + /// bool - the result of the LessThanOrClose comparision. + /// + /// The first double to compare. + /// The second double to compare. + public static bool LessThanOrClose(double value1, double value2) + { + return (value1 < value2) || AreClose(value1, value2); + } + + /// + /// GreaterThanOrClose - Returns whether or not the first double is greater than or close to + /// the second double. That is, whether or not the first is strictly greater than or within + /// epsilon of the other number. Note that this epsilon is proportional to the numbers + /// themselves to that AreClose survives scalar multiplication. Note, + /// There are plenty of ways for this to return false even for numbers which + /// are theoretically identical, so no code calling this should fail to work if this + /// returns false. This is important enough to repeat: + /// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be + /// used for optimizations *only*. + /// + /// + /// bool - the result of the GreaterThanOrClose comparision. + /// + /// The first double to compare. + /// The second double to compare. + public static bool GreaterThanOrClose(double value1, double value2) + { + return (value1 > value2) || AreClose(value1, value2); + } + + /// + /// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1), + /// but this is faster. + /// + /// + /// bool - the result of the AreClose comparision. + /// + /// The double to compare to 1. + public static bool IsOne(double value) + { + return Math.Abs(value - 1.0) < 10.0 * DBL_EPSILON; + } + + /// + /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0), + /// but this is faster. + /// + /// + /// bool - the result of the AreClose comparision. + /// + /// The double to compare to 0. + public static bool IsZero(double value) + { + return Math.Abs(value) < 10.0 * DBL_EPSILON; + } + + // The Point, Size, Rect and Matrix class have moved to WinCorLib. However, we provide + // internal AreClose methods for our own use here. + + /// + /// Compares two points for fuzzy equality. This function + /// helps compensate for the fact that double values can + /// acquire error when operated upon + /// + /// The first point to compare + /// The second point to compare + /// Whether or not the two points are equal + public static bool AreClose(Point point1, Point point2) + { + return DoubleUtil.AreClose(point1.X, point2.X) && + DoubleUtil.AreClose(point1.Y, point2.Y); + } + + /// + /// Compares two Size instances for fuzzy equality. This function + /// helps compensate for the fact that double values can + /// acquire error when operated upon + /// + /// The first size to compare + /// The second size to compare + /// Whether or not the two Size instances are equal + public static bool AreClose(Size size1, Size size2) + { + return DoubleUtil.AreClose(size1.Width, size2.Width) && + DoubleUtil.AreClose(size1.Height, size2.Height); + } + + /// + /// Compares two Vector instances for fuzzy equality. This function + /// helps compensate for the fact that double values can + /// acquire error when operated upon + /// + /// The first Vector to compare + /// The second Vector to compare + /// Whether or not the two Vector instances are equal + public static bool AreClose(Vector vector1, Vector vector2) + { + return DoubleUtil.AreClose(vector1.X, vector2.X) && + DoubleUtil.AreClose(vector1.Y, vector2.Y); + } + + /// + /// + /// + /// + /// + public static bool IsBetweenZeroAndOne(double val) + { + return (GreaterThanOrClose(val, 0) && LessThanOrClose(val, 1)); + } + + /// + /// + /// + /// + /// + public static int DoubleToInt(double val) + { + return (0 < val) ? (int) (val + 0.5) : (int) (val - 0.5); + } + + +#if !PBTCOMPILER + + [StructLayout(LayoutKind.Explicit)] + private struct NanUnion + { + [FieldOffset(0)] internal double DoubleValue; + [FieldOffset(0)] internal UInt64 UintValue; + } + + // The standard CLR double.IsNaN() function is approximately 100 times slower than our own wrapper, + // so please make sure to use DoubleUtil.IsNaN() in performance sensitive code. + // PS item that tracks the CLR improvement is DevDiv Schedule : 26916. + // IEEE 754 : If the argument is any value in the range 0x7ff0000000000001L through 0x7fffffffffffffffL + // or in the range 0xfff0000000000001L through 0xffffffffffffffffL, the result will be NaN. + public static bool IsNaN(double value) + { + NanUnion t = new NanUnion(); + t.DoubleValue = value; + + UInt64 exp = t.UintValue & 0xfff0000000000000; + UInt64 man = t.UintValue & 0x000fffffffffffff; + + return (exp == 0x7ff0000000000000 || exp == 0xfff0000000000000) && (man != 0); + } +#endif + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/Generated/Matrix.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/Generated/Matrix.cs new file mode 100644 index 0000000..bb08d6d --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/Generated/Matrix.cs @@ -0,0 +1,233 @@ +// 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. + +// +// +// This file was generated, please do not edit it directly. +// +// Please see MilCodeGen.html for more information. +// + +using System; +//using System.Windows.Media.Converters; +// These types are aliased to match the unamanaged names used in interop + +namespace WpfInk.WindowsBase.System.Windows.Media +{ + partial struct Matrix + { + #region Public Methods + + /// + /// Compares two Matrix 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. + /// + /// + /// bool - true if the two Matrix instances are exactly equal, false otherwise + /// + /// The first Matrix to compare + /// The second Matrix to compare + public static bool operator ==(Matrix matrix1, Matrix matrix2) + { + if (matrix1.IsDistinguishedIdentity || matrix2.IsDistinguishedIdentity) + { + return matrix1.IsIdentity == matrix2.IsIdentity; + } + else + { + return matrix1.M11 == matrix2.M11 && + matrix1.M12 == matrix2.M12 && + matrix1.M21 == matrix2.M21 && + matrix1.M22 == matrix2.M22 && + matrix1.OffsetX == matrix2.OffsetX && + matrix1.OffsetY == matrix2.OffsetY; + } + } + + /// + /// Compares two Matrix 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 Matrix instances are exactly unequal, false otherwise + /// + /// The first Matrix to compare + /// The second Matrix to compare + public static bool operator !=(Matrix matrix1, Matrix matrix2) + { + return !(matrix1 == matrix2); + } + + /// + /// Compares two Matrix instances for object equality. 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 the two Matrix instances are exactly equal, false otherwise + /// + /// The first Matrix to compare + /// The second Matrix to compare + public static bool Equals(Matrix matrix1, Matrix matrix2) + { + if (matrix1.IsDistinguishedIdentity || matrix2.IsDistinguishedIdentity) + { + return matrix1.IsIdentity == matrix2.IsIdentity; + } + else + { + return matrix1.M11.Equals(matrix2.M11) && + matrix1.M12.Equals(matrix2.M12) && + matrix1.M21.Equals(matrix2.M21) && + matrix1.M22.Equals(matrix2.M22) && + matrix1.OffsetX.Equals(matrix2.OffsetX) && + matrix1.OffsetY.Equals(matrix2.OffsetY); + } + } + + /// + /// Equals - compares this Matrix 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 the object is an instance of Matrix and if it's equal to "this". + /// + /// The object to compare to "this" + public override bool Equals(object o) + { + if ((null == o) || !(o is Matrix)) + { + return false; + } + + Matrix value = (Matrix) o; + return Matrix.Equals(this, value); + } + + /// + /// Equals - compares this Matrix 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 Matrix to compare to "this" + public bool Equals(Matrix value) + { + return Matrix.Equals(this, value); + } + + /// + /// Returns the HashCode for this Matrix + /// + /// + /// int - the HashCode for this Matrix + /// + public override int GetHashCode() + { + if (IsDistinguishedIdentity) + { + return c_identityHashCode; + } + else + { + // Perform field-by-field XOR of HashCodes + return M11.GetHashCode() ^ + M12.GetHashCode() ^ + M21.GetHashCode() ^ + M22.GetHashCode() ^ + OffsetX.GetHashCode() ^ + OffsetY.GetHashCode(); + } + } + + #endregion Public Methods + + #region Internal Properties + + /// + /// Creates a string representation of this object based on the current culture. + /// + /// + /// A string representation of this object. + /// + public override string ToString() + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, null /* format provider */); + } + + /// + /// Creates a string representation of this object based on the IFormatProvider + /// passed in. If the provider is null, the CurrentCulture is used. + /// + /// + /// A string representation of this object. + /// + public string ToString(IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(null /* format string */, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + string IFormattable.ToString(string format, IFormatProvider provider) + { + // Delegate to the internal method which implements all ToString calls. + return ConvertToString(format, provider); + } + + /// + /// Creates a string representation of this object based on the format string + /// and IFormatProvider passed in. + /// If the provider is null, the CurrentCulture is used. + /// See the documentation for IFormattable for more information. + /// + /// + /// A string representation of this object. + /// + internal string ConvertToString(string format, IFormatProvider provider) + { + if (IsIdentity) + { + return "Identity"; + } + + // Helper to get the numeric list separator for a given culture. + char separator = ','; //MS.Internal.TokenizerHelper.GetNumericListSeparator(provider); + return String.Format(provider, + "{1:" + format + "}{0}{2:" + format + "}{0}{3:" + format + "}{0}{4:" + format + "}{0}{5:" + format + + "}{0}{6:" + format + "}", + separator, + _m11, + _m12, + _m21, + _m22, + _offsetX, + _offsetY); + } + + #endregion Internal Properties + } +} \ No newline at end of file diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/KnownIds.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/KnownIds.cs new file mode 100644 index 0000000..e9ccd37 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/KnownIds.cs @@ -0,0 +1,264 @@ +// 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; +using MS.Internal.Ink.InkSerializedFormat; + +namespace WpfInk.PresentationCore.System.Windows.Ink +{ + /// + /// [To be supplied.] + /// + internal static class KnownIds + { + #region Public Ids + /// + /// [To be supplied.] + /// + internal static readonly Guid X = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.X]; + /// + /// [To be supplied.] + /// + internal static readonly Guid Y = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.Y]; + /// + /// [To be supplied.] + /// + internal static readonly Guid Z = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.Z]; + /// + /// [To be supplied.] + /// + internal static readonly Guid PacketStatus = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.PacketStatus]; + /// + /// [To be supplied.] + /// + internal static readonly Guid TimerTick = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.TimerTick]; + /// + /// [To be supplied.] + /// + internal static readonly Guid SerialNumber = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.SerialNumber]; + /// + /// [To be supplied.] + /// + internal static readonly Guid NormalPressure = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.NormalPressure]; + /// + /// [To be supplied.] + /// + internal static readonly Guid TangentPressure = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.TangentPressure]; + /// + /// [To be supplied.] + /// + internal static readonly Guid ButtonPressure = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.ButtonPressure]; + /// + /// [To be supplied.] + /// + internal static readonly Guid XTiltOrientation = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.XTiltOrientation]; + /// + /// [To be supplied.] + /// + internal static readonly Guid YTiltOrientation = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.YTiltOrientation]; + /// + /// [To be supplied.] + /// + internal static readonly Guid AzimuthOrientation = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.AzimuthOrientation]; + /// + /// [To be supplied.] + /// + internal static readonly Guid AltitudeOrientation = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.AltitudeOrientation]; + /// + /// [To be supplied.] + /// + internal static readonly Guid TwistOrientation = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.TwistOrientation]; + /// + /// [To be supplied.] + /// + internal static readonly Guid PitchRotation = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.PitchRotation]; + /// + /// [To be supplied.] + /// + internal static readonly Guid RollRotation = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.RollRotation]; + /// + /// [To be supplied.] + /// + internal static readonly Guid YawRotation = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.YawRotation]; + /// + /// [To be supplied.] + /// + internal static readonly Guid Color = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.ColorRef]; + /// + /// [To be supplied.] + /// + internal static readonly Guid DrawingFlags = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.DrawingFlags]; + /// + /// [To be supplied.] + /// + internal static readonly Guid CursorId = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.CursorId]; + /// + /// [To be supplied.] + /// + internal static readonly Guid WordAlternates = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.WordAlternates]; + /// + /// [To be supplied.] + /// + internal static readonly Guid CharacterAlternates = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.CharAlternates]; + /// + /// [To be supplied.] + /// + internal static readonly Guid InkMetrics = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.InkMetrics]; + /// + /// [To be supplied.] + /// + internal static readonly Guid GuideStructure = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.GuideStructure]; + /// + /// [To be supplied.] + /// + internal static readonly Guid Timestamp = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.Timestamp]; + /// + /// [To be supplied.] + /// + internal static readonly Guid Language = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.Language]; + /// + /// [To be supplied.] + /// + internal static readonly Guid Transparency = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.Transparency]; + /// + /// [To be supplied.] + /// + internal static readonly Guid CurveFittingError = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.CurveFittingError]; + /// + /// [To be supplied.] + /// + internal static readonly Guid RecognizedLattice = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.RecoLattice]; + /// + /// [To be supplied.] + /// + internal static readonly Guid CursorDown = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.CursorDown]; + /// + /// [To be supplied.] + /// + internal static readonly Guid SecondaryTipSwitch = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.SecondaryTipSwitch]; + /// + /// [To be supplied.] + /// + internal static readonly Guid TabletPick = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.TabletPick]; + /// + /// [To be supplied.] + /// + internal static readonly Guid BarrelDown = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.BarrelDown]; + /// + /// [To be supplied.] + /// + internal static readonly Guid RasterOperation = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.RasterOperation]; + + /// + /// The height of the pen tip which affects the stroke rendering. + /// + internal static readonly Guid StylusHeight = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.StylusHeight]; + + /// + /// The width of the pen tip which affects the stroke rendering. + /// + internal static readonly Guid StylusWidth = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.StylusWidth]; + + /// + /// Guid identifying the highlighter property + /// + internal static readonly Guid Highlighter = KnownIdCache.TabletInternalIdTable[(int) KnownIdCache.TabletInternalIdIndex.Highlighter]; + /// + /// Guid identifying the Ink properties + /// + internal static readonly Guid InkProperties = KnownIdCache.TabletInternalIdTable[(int) KnownIdCache.TabletInternalIdIndex.InkProperties]; + /// + /// Guid identifying the Ink Style bold property + /// + internal static readonly Guid InkStyleBold = KnownIdCache.TabletInternalIdTable[(int) KnownIdCache.TabletInternalIdIndex.InkStyleBold]; + /// + /// Guid identifying the ink style italics property + /// + internal static readonly Guid InkStyleItalics = KnownIdCache.TabletInternalIdTable[(int) KnownIdCache.TabletInternalIdIndex.InkStyleItalics]; + /// + /// Guid identifying the stroke timestamp property + /// + internal static readonly Guid StrokeTimestamp = KnownIdCache.TabletInternalIdTable[(int) KnownIdCache.TabletInternalIdIndex.StrokeTimestamp]; + /// + /// Guid identifying the stroke timeid property + /// + internal static readonly Guid StrokeTimeId = KnownIdCache.TabletInternalIdTable[(int) KnownIdCache.TabletInternalIdIndex.StrokeTimeId]; + + /// + /// Guid identifying the StylusTip + /// + internal static readonly Guid StylusTip = new Guid(0x3526c731, 0xee79, 0x4988, 0xb9, 0x3e, 0x70, 0xd9, 0x2f, 0x89, 0x7, 0xed); + + /// + /// Guid identifying the StylusTipTransform + /// + internal static readonly Guid StylusTipTransform = new Guid(0x4b63bc16, 0x7bc4, 0x4fd2, 0x95, 0xda, 0xac, 0xff, 0x47, 0x75, 0x73, 0x2d); + + + /// + /// Guid identifying IsHighlighter + /// + internal static readonly Guid IsHighlighter = new Guid(0xce305e1a, 0xe08, 0x45e3, 0x8c, 0xdc, 0xe4, 0xb, 0xb4, 0x50, 0x6f, 0x21); + + // /// + // /// Guid used for identifying the fill-brush for rendering a stroke. + // /// + // public static readonly Guid FillBrush = new Guid(0x9a547c5c, 0x1fff, 0x4987, 0x8a, 0xb6, 0xbe, 0xed, 0x75, 0xde, 0xa, 0x1d); + // + // /// + // /// Guid used for identifying the pen used for rendering a stroke's outline. + // /// + // public static readonly Guid OutlinePen = new Guid(0x9967aea6, 0x3980, 0x4337, 0xb7, 0xc6, 0x34, 0xa, 0x33, 0x98, 0x8e, 0x6b); + // + // /// + // /// Guid used for identifying the blend mode used for rendering a stroke (similar to ROP in v1). + // /// + // public static readonly Guid BlendMode = new Guid(0xd6993943, 0x7a84, 0x4a80, 0x84, 0x68, 0xa8, 0x3c, 0xca, 0x65, 0xb0, 0x5); + // + // /// + // /// Guid used for identifying StylusShape object + // /// + // public static readonly Guid StylusShape = new Guid(0xf998e7f8, 0x7cdb, 0x4c0e, 0xb2, 0xe2, 0x63, 0x2b, 0xca, 0x21, 0x2a, 0x7b); + #endregion + + #region Internal Ids + + /// + /// The style of the rendering used for the pen tip. + /// + internal static readonly Guid PenStyle = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.PenStyle]; + + /// + /// The shape of the tip of the pen used for stroke rendering. + /// + internal static readonly Guid PenTip = KnownIdCache.OriginalISFIdTable[(int) KnownIdCache.OriginalISFIdIndex.PenTip]; + + /// + /// Guid used for identifying the Custom Stroke + /// + /// Should we hide the CustomStrokes and StrokeLattice data? + internal static readonly Guid InkCustomStrokes = KnownIdCache.TabletInternalIdTable[(int) KnownIdCache.TabletInternalIdIndex.InkCustomStrokes]; + + /// + /// Guid used for identifying the Stroke Lattice + /// + internal static readonly Guid InkStrokeLattice = KnownIdCache.TabletInternalIdTable[(int) KnownIdCache.TabletInternalIdIndex.InkStrokeLattice]; + +#if UNDO_ENABLED + /// + /// Guid used for identifying if an undo/event has already been handled + /// + /// {053BF717-DBE7-4e52-805E-64906138FAAD} + internal static readonly Guid UndoEventArgsHandled = new Guid(0x53bf717, 0xdbe7, 0x4e52, 0x80, 0x5e, 0x64, 0x90, 0x61, 0x38, 0xfa, 0xad); +#endif + #endregion + + #region Known Id Helpers + private static global::System.Reflection.MemberInfo[] PublicMemberInfo = null; + + #endregion + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/MS.Internal.PresentationCore.SRID .cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/MS.Internal.PresentationCore.SRID .cs new file mode 100644 index 0000000..562af65 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/MS.Internal.PresentationCore.SRID .cs @@ -0,0 +1,1615 @@ +// +using System.Reflection; + +namespace FxResources.PresentationCore +{ + internal static class SR { } +} +namespace MS.Internal.PresentationCore +{ + internal static partial class SRID + { + private static global::System.Resources.ResourceManager s_resourceManager; + internal static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager(typeof(FxResources.PresentationCore.SR))); + internal static global::System.Globalization.CultureInfo Culture { get; set; } +#if !NET20 + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] +#endif + internal static string GetResourceString(string resourceKey, string defaultValue = null) => ResourceManager.GetString(resourceKey, Culture); + /// '{0}' is not a single Unicode character. + internal const string @AccessKeyManager_NotAUnicodeCharacter = "AccessKeyManager_NotAUnicodeCharacter"; + /// Text formatting engine cannot acquire break record due to error: '{0}'. + internal const string @AcquireBreakRecordFailure = "AcquireBreakRecordFailure"; + /// Text formatting engine cannot acquire text penalty module due to error: '{0}'. + internal const string @AcquirePenaltyModuleFailure = "AcquirePenaltyModuleFailure"; + /// Cannot add text to '{0}'. + internal const string @AddText_Invalid = "AddText_Invalid"; + /// If AllGestures is specified, it must be the only ApplicationGesture in the ApplicationGesture array. + internal const string @AllGesturesMustExistAlone = "AllGesturesMustExistAlone"; + /// AnimationTimeline of type '{0}' cannot be used to animate the '{1}' property of type '{2}'. + internal const string @Animation_AnimationTimelineTypeMismatch = "Animation_AnimationTimelineTypeMismatch"; + /// The animation(s) applied to the '{0}' property calculate a current value of '{1}', which is not a valid value for the property. + internal const string @Animation_CalculatedValueIsInvalidForProperty = "Animation_CalculatedValueIsInvalidForProperty"; + /// A child of KeyFrameAnimation in XAML must be a KeyFrame of a compatible type. + internal const string @Animation_ChildMustBeKeyFrame = "Animation_ChildMustBeKeyFrame"; + /// One of the animations in the timeline is a '{0}' and cannot be used to animate a property of type '{1}'. + internal const string @Animation_ChildTypeMismatch = "Animation_ChildTypeMismatch"; + /// '{0}' property is not animatable on '{1}' class because the IsAnimationProhibited flag has been set on the UIPropertyMetadata used to associate the property with the class. + internal const string @Animation_DependencyPropertyIsNotAnimatable = "Animation_DependencyPropertyIsNotAnimatable"; + /// Cannot animate the '{0}' property on a '{1}' using a '{2}'. For details see the inner exception. + internal const string @Animation_Exception = "Animation_Exception"; + /// '{0}' is not a valid '{1}' value for class '{2}'. This value might have been supplied by the base value of the property being animated or the output value of another animation applied to the same property. + internal const string @Animation_InvalidBaseValue = "Animation_InvalidBaseValue"; + /// Resolved KeyTime for key frame at index {1} cannot be greater than resolved KeyTime for key frame at index {4}. KeyFrames[{1}] has specified KeyTime '{2}', which resolves to time {3}Animation_InvalidAnimationUsingKeyFramesDuration + internal const string @Animation_InvalidResolvedKeyTimes = "Animation_InvalidResolvedKeyTimes"; + /// '{2}' KeyTime value is not valid for key frame at index {1} of this '{0}' because it is greater than animation's Duration value '{3}'. + internal const string @Animation_InvalidTimeKeyTime = "Animation_InvalidTimeKeyTime"; + /// '{0}' cannot use default {1} value of '{2}'. + internal const string @Animation_Invalid_DefaultValue = "Animation_Invalid_DefaultValue"; + /// Cannot set '{0}' to '{1}'. KeySpline values must be between 0.0 and 1.0. + internal const string @Animation_KeySpline_InvalidValue = "Animation_KeySpline_InvalidValue"; + /// '{0}' is not a valid Percent value for a KeyTime. The Percent value must be a number from 0.0 to 1.0. + internal const string @Animation_KeyTime_InvalidPercentValue = "Animation_KeyTime_InvalidPercentValue"; + /// Cannot create a KeyTime with the value '{0}' because it is less than zero. + internal const string @Animation_KeyTime_LessThanZero = "Animation_KeyTime_LessThanZero"; + /// '{0}' value is not valid because it contains no animations. + internal const string @Animation_NoAnimationsSpecified = "Animation_NoAnimationsSpecified"; + /// KeyFrameAnimation objects cannot have text objects as children. + internal const string @Animation_NoTextChildren = "Animation_NoTextChildren"; + /// A '{0}' on the '{1}' property of a '{2}' returned a current value of UnsetValue.Instance, which is not valid. + internal const string @Animation_ReturnedUnsetValueInstance = "Animation_ReturnedUnsetValueInstance"; + /// The HandoffBehavior value is not valid. + internal const string @Animation_UnrecognizedHandoffBehavior = "Animation_UnrecognizedHandoffBehavior"; + /// This AnimationEffect is already attached to a UIElement. + internal const string @AnimEffect_AlreadyAttached = "AnimEffect_AlreadyAttached"; + /// This AnimationEffectCollection is already being used by another UIElement. + internal const string @AnimEffect_CollectionInUse = "AnimEffect_CollectionInUse"; + /// This AnimationEffect is not attached to a Visual. + internal const string @AnimEffect_NoVisual = "AnimEffect_NoVisual"; + /// The ApplicationGesture array must contain at least one member. + internal const string @ApplicationGestureArrayLengthIsZero = "ApplicationGestureArrayLengthIsZero"; + /// The specified ApplicationGesture is not valid. + internal const string @ApplicationGestureIsInvalid = "ApplicationGestureIsInvalid"; + /// Automation client cannot access UI because application is shutting down. + internal const string @AutomationDispatcherShutdown = "AutomationDispatcherShutdown"; + /// Timeout occurred while attempting to access UI. The application might be busy or unresponsive. + internal const string @AutomationTimeout = "AutomationTimeout"; + /// '{0}' is not a valid System.Windows.Automation.AutomationPeer. It is expected to be associated with a Window known to Automation. + internal const string @Automation_InvalidConnectedPeer = "Automation_InvalidConnectedPeer"; + /// '{0}' is not a valid System.Windows.Automation.AutomationEvent. + internal const string @Automation_InvalidEventId = "Automation_InvalidEventId"; + /// '{0}' is not a valid System.Windows.Automation.SynchronizedInputType. + internal const string @Automation_InvalidSynchronizedInputType = "Automation_InvalidSynchronizedInputType"; + /// Recursive call to Automation Peer API is not valid. + internal const string @Automation_RecursivePublicCall = "Automation_RecursivePublicCall"; + /// Unsupported UI Automation event association. + internal const string @Automation_UnsupportedUIAutomationEventAssociation = "Automation_UnsupportedUIAutomationEventAssociation"; + /// BitmapCacheBrush does not support Opacity. + internal const string @BitmapCacheBrush_OpacityChanged = "BitmapCacheBrush_OpacityChanged"; + /// BitmapCacheBrush does not support RelativeTransform. + internal const string @BitmapCacheBrush_RelativeTransformChanged = "BitmapCacheBrush_RelativeTransformChanged"; + /// BitmapCacheBrush does not support Transform. + internal const string @BitmapCacheBrush_TransformChanged = "BitmapCacheBrush_TransformChanged"; + /// Alt+Left;Backspace + internal const string @BrowseBackKeyDisplayString = "BrowseBackKeyDisplayString"; + /// Back + internal const string @BrowseBackText = "BrowseBackText"; + /// Alt+Right;Shift+Backspace + internal const string @BrowseForwardKeyDisplayString = "BrowseForwardKeyDisplayString"; + /// Forward + internal const string @BrowseForwardText = "BrowseForwardText"; + /// Alt+Home;BrowserHome + internal const string @BrowseHomeKeyDisplayString = "BrowseHomeKeyDisplayString"; + /// Home + internal const string @BrowseHomeText = "BrowseHomeText"; + /// Alt+Esc;BrowserStop + internal const string @BrowseStopKeyDisplayString = "BrowseStopKeyDisplayString"; + /// Stop + internal const string @BrowseStopText = "BrowseStopText"; + /// Unrecognized brush type in BAML file. + internal const string @BrushUnknownBamlType = "BrushUnknownBamlType"; + /// Cannot access a disposed HTTP byte range downloader. + internal const string @ByteRangeDownloaderDisposed = "ByteRangeDownloaderDisposed"; + /// Byte range request failed. + internal const string @ByteRangeDownloaderErroredOut = "ByteRangeDownloaderErroredOut"; + /// Server does not support byte range request. + internal const string @ByteRangeRequestIsNotSupported = "ByteRangeRequestIsNotSupported"; + /// Cancel Print + internal const string @CancelPrintText = "CancelPrintText"; + /// Cannot attach a Visual that is already attached. + internal const string @CannotAttachVisualTwice = "CannotAttachVisualTwice"; + /// '{0}' and '{1}' cannot both be null. + internal const string @CannotBothBeNull = "CannotBothBeNull"; + /// Cannot convert string value '{0}' to type '{1}'. + internal const string @CannotConvertStringToType = "CannotConvertStringToType"; + /// Cannot convert type '{0}' to '{1}'. + internal const string @CannotConvertType = "CannotConvertType"; + /// Cannot modify a read-only container. + internal const string @CannotModifyReadOnlyContainer = "CannotModifyReadOnlyContainer"; + /// Cannot modify the Visual children for this node because a tree walk is in progress. + internal const string @CannotModifyVisualChildrenDuringTreeWalk = "CannotModifyVisualChildrenDuringTreeWalk"; + /// Cannot navigate to application resource '{0}' by using a WebBrowser control. For URI navigation, the resource must be at the application's site of origin. Use the pack://siteoforigin:,,,/ prefix to avoid hard-coding the URI. + internal const string @CannotNavigateToApplicationResourcesInWebBrowser = "CannotNavigateToApplicationResourcesInWebBrowser"; + /// Cannot get part or part information from a write-only container. + internal const string @CannotRetrievePartsOfWriteOnlyContainer = "CannotRetrievePartsOfWriteOnlyContainer"; + /// Cannot read from the specified command buffer pointer. + internal const string @Channel_InvalidCommandBufferPointer = "Channel_InvalidCommandBufferPointer"; + /// The Metrics property of CharacterMetrics is missing a required field. + internal const string @CharacterMetrics_MissingRequiredField = "CharacterMetrics_MissingRequiredField"; + /// CharacterMetrics is not valid. The horizontal advance (defined as the sum of BlackBoxWidth, LeftSideBearing, and RightSideBearing) cannot be negative. + internal const string @CharacterMetrics_NegativeHorizontalAdvance = "CharacterMetrics_NegativeHorizontalAdvance"; + /// CharacterMetrics is not valid. The vertical advance (defined as the sum of BlackBoxHeight, TopSideBearing, and BottomSideBearing) cannot be negative. + internal const string @CharacterMetrics_NegativeVerticalAdvance = "CharacterMetrics_NegativeVerticalAdvance"; + /// The Metrics property of CharacterMetrics has too many fields. + internal const string @CharacterMetrics_TooManyFields = "CharacterMetrics_TooManyFields"; + /// Class handlers can be registered only for UIElement or ContentElement and their subtypes. + internal const string @ClassTypeIllegal = "ClassTypeIllegal"; + /// Text formatting engine cannot clone break record due to error: '{0}'. + internal const string @CloneBreakRecordFailure = "CloneBreakRecordFailure"; + /// Close + internal const string @CloseText = "CloseText"; + /// A cluster map entry must be greater than or equal to a previous entry. + internal const string @ClusterMapEntriesShouldNotDecrease = "ClusterMapEntriesShouldNotDecrease"; + /// A cluster map entry must point to a valid glyph indices element. + internal const string @ClusterMapEntryShouldPointWithinGlyphIndices = "ClusterMapEntryShouldPointWithinGlyphIndices"; + /// The first element in the cluster map must equal zero. + internal const string @ClusterMapFirstEntryMustBeZero = "ClusterMapFirstEntryMustBeZero"; + /// '{0}' character is outside the Unicode code point range. + internal const string @CodePointOutOfRange = "CodePointOutOfRange"; + /// '{0}' key already exists in the collection. + internal const string @CollectionDuplicateKey = "CollectionDuplicateKey"; + /// Collection was modified during enumeration. + internal const string @CollectionEnumerationError = "CollectionEnumerationError"; + /// This collection is fixed size. + internal const string @CollectionIsFixedSize = "CollectionIsFixedSize"; + /// The number of elements in this collection must be greater than zero. + internal const string @CollectionNumberOfElementsMustBeGreaterThanZero = "CollectionNumberOfElementsMustBeGreaterThanZero"; + /// The number of elements in this collection must be less than or equal to '{0}'. + internal const string @CollectionNumberOfElementsMustBeLessOrEqualTo = "CollectionNumberOfElementsMustBeLessOrEqualTo"; + /// The number of elements in this collection should equal '{0}'. + internal const string @CollectionNumberOfElementsShouldBeEqualTo = "CollectionNumberOfElementsShouldBeEqualTo"; + /// Collection accepts only objects of type CommandBinding. + internal const string @CollectionOnlyAcceptsCommandBindings = "CollectionOnlyAcceptsCommandBindings"; + /// Collection accepts only objects of type InputBinding. + internal const string @CollectionOnlyAcceptsInputBindings = "CollectionOnlyAcceptsInputBindings"; + /// Collection accepts only objects of type InputGesture. + internal const string @CollectionOnlyAcceptsInputGestures = "CollectionOnlyAcceptsInputGestures"; + /// Destination array is not compatible with objects within '{0}'. + internal const string @Collection_BadDestArray = "Collection_BadDestArray"; + /// Input array is not a valid rank. + internal const string @Collection_BadRank = "Collection_BadRank"; + /// Cannot add instance of type '{1}' to a collection of type '{0}'. Only items of type '{2}' are allowed. + internal const string @Collection_BadType = "Collection_BadType"; + /// Cannot pass multidimensional array to the CopyTo method on a collection. + internal const string @Collection_CopyTo_ArrayCannotBeMultidimensional = "Collection_CopyTo_ArrayCannotBeMultidimensional"; + /// '{0}' parameter value is equal to or greater than the length of the '{1}' parameter value. + internal const string @Collection_CopyTo_IndexGreaterThanOrEqualToArrayLength = "Collection_CopyTo_IndexGreaterThanOrEqualToArrayLength"; + /// The number of elements in this collection is greater than the available space from '{0}' to the end of destination '{1}'. + internal const string @Collection_CopyTo_NumberOfElementsExceedsArrayLength = "Collection_CopyTo_NumberOfElementsExceedsArrayLength"; + /// Cannot add null to the collection. + internal const string @Collection_NoNull = "Collection_NoNull"; + /// File is too large to be a valid ColorContext. + internal const string @ColorContext_FileTooLarge = "ColorContext_FileTooLarge"; + /// Color context must be sRGB or scRGB for this operation. + internal const string @Color_ColorContextNotsRGB_or_scRGB = "Color_ColorContextNotsRGB_or_scRGB"; + /// Color context types mismatch. + internal const string @Color_ColorContextTypeMismatch = "Color_ColorContextTypeMismatch"; + /// Color context dimensions mismatch. + internal const string @Color_DimensionMismatch = "Color_DimensionMismatch"; + /// Color context is null. + internal const string @Color_NullColorContext = "Color_NullColorContext"; + /// The property '{0}' cannot be changed. The '{1}' class has been sealed. + internal const string @CompatibilityPreferencesSealed = "CompatibilityPreferencesSealed"; + /// Typography properties are not valid. + internal const string @CompileFeatureSet_InvalidTypographyProperties = "CompileFeatureSet_InvalidTypographyProperties"; + /// Invalid value for {0} attribute. + internal const string @CompositeFontAttributeValue1 = "CompositeFontAttributeValue1"; + /// Invalid value for {0} attribute: {1} + internal const string @CompositeFontAttributeValue2 = "CompositeFontAttributeValue2"; + /// Unicode range is not valid. + internal const string @CompositeFontInvalidUnicodeRange = "CompositeFontInvalidUnicodeRange"; + /// Missing required attribute '{0}'. + internal const string @CompositeFontMissingAttribute = "CompositeFontMissingAttribute"; + /// Missing required element '{0}'. + internal const string @CompositeFontMissingElement = "CompositeFontMissingElement"; + /// The composite font contains significant whitespace where none is expected. + internal const string @CompositeFontSignificantWhitespace = "CompositeFontSignificantWhitespace"; + /// '{0}' attribute in XML namespace '{1}' not recognized. Note that attribute names are case sensitive. + internal const string @CompositeFontUnknownAttribute = "CompositeFontUnknownAttribute"; + /// '{0}' element in XML namespace '{1}' not recognized. Note that element names are case sensitive. + internal const string @CompositeFontUnknownElement = "CompositeFontUnknownElement"; + /// A FontFamily cannot have more than one FamilyTypeface with the same Style, Weight, and Stretch. + internal const string @CompositeFont_DuplicateTypeface = "CompositeFont_DuplicateTypeface"; + /// The FontFamily cannot hold any more FamilyMaps. + internal const string @CompositeFont_TooManyFamilyMaps = "CompositeFont_TooManyFamilyMaps"; + /// The root Visual of a VisualTarget cannot have a parent. + internal const string @CompositionTarget_RootVisual_HasParent = "CompositionTarget_RootVisual_HasParent"; + /// Possible constructor recursion detected. + internal const string @ConstructorRecursion = "ConstructorRecursion"; + /// Shift+F10;Apps + internal const string @ContextMenuKeyDisplayString = "ContextMenuKeyDisplayString"; + /// Context Menu + internal const string @ContextMenuText = "ContextMenuText"; + /// Cannot convert from type. + internal const string @Converter_ConvertFromNotSupported = "Converter_ConvertFromNotSupported"; + /// Cannot convert to type. + internal const string @Converter_ConvertToNotSupported = "Converter_ConvertToNotSupported"; + /// Ctrl+C;Ctrl+Insert + internal const string @CopyKeyDisplayString = "CopyKeyDisplayString"; + /// Copy + internal const string @CopyText = "CopyText"; + /// + internal const string @CorrectionListKey = "CorrectionListKey"; + /// + internal const string @CorrectionListKeyDisplayString = "CorrectionListKeyDisplayString"; + /// Correction List + internal const string @CorrectionListText = "CorrectionListText"; + /// Count must be less than or equal to remaining number of bits in stream. + internal const string @CountOfBitsGreatThanRemainingBits = "CountOfBitsGreatThanRemainingBits"; + /// Count must be less than or equal to bits per byte and greater than zero. + internal const string @CountOfBitsOutOfRange = "CountOfBitsOutOfRange"; + /// Text formatting engine cannot format breakpoints due to error: '{0}'. + internal const string @CreateBreaksFailure = "CreateBreaksFailure"; + /// Text formatting engine cannot create text formatting context due to error: '{0}'. + internal const string @CreateContextFailure = "CreateContextFailure"; + /// Text formatting engine cannot format a line of text due to error: '{0}'. + internal const string @CreateLineFailure = "CreateLineFailure"; + /// Text formatting engine cannot format a paragraph cache due to error: '{0}'. + internal const string @CreateParaBreakingSessionFailure = "CreateParaBreakingSessionFailure"; + /// Current dispatcher cannot be found. + internal const string @CurrentDispatcherNotFound = "CurrentDispatcherNotFound"; + /// Failed to load cursor from the stream. + internal const string @Cursor_InvalidStream = "Cursor_InvalidStream"; + /// Failed to load cursor file '{0}'. + internal const string @Cursor_LoadImageFailure = "Cursor_LoadImageFailure"; + /// '{0}' has unsupported extension for cursor. + internal const string @Cursor_UnsupportedFormat = "Cursor_UnsupportedFormat"; + /// Ctrl+X;Shift+Delete + internal const string @CutKeyDisplayString = "CutKeyDisplayString"; + /// Cut + internal const string @CutText = "CutText"; + /// An antialiased back buffer requires a IDirect3DDevice9Ex device. + internal const string @D3DImage_AARequires9Ex = "D3DImage_AARequires9Ex"; + /// Back buffer's device is not valid. + internal const string @D3DImage_InvalidDevice = "D3DImage_InvalidDevice"; + /// Back buffer's pool does not meet the requirements for the resource type. + internal const string @D3DImage_InvalidPool = "D3DImage_InvalidPool"; + /// Back buffer's usage does not meet the requirements for the resource type. + internal const string @D3DImage_InvalidUsage = "D3DImage_InvalidUsage"; + /// Cannot call this method without a back buffer. + internal const string @D3DImage_MustHaveBackBuffer = "D3DImage_MustHaveBackBuffer"; + /// Back buffer's size is too large. + internal const string @D3DImage_SurfaceTooBig = "D3DImage_SurfaceTooBig"; + /// Cannot SetData on a frozen OLE data object. + internal const string @DataObject_CannotSetDataOnAFozenOLEDataDbject = "DataObject_CannotSetDataOnAFozenOLEDataDbject"; + /// '{0}' data format is not present on DataObject. + internal const string @DataObject_DataFormatNotPresentOnDataObject = "DataObject_DataFormatNotPresentOnDataObject"; + /// Data object must have at least one format. + internal const string @DataObject_DataObjectMustHaveAtLeastOneFormat = "DataObject_DataObjectMustHaveAtLeastOneFormat"; + /// Empty string is not a valid value for parameter 'format'. + internal const string @DataObject_EmptyFormatNotAllowed = "DataObject_EmptyFormatNotAllowed"; + /// '{0}' file drop path is not valid. + internal const string @DataObject_FileDropListHasInvalidFileDropPath = "DataObject_FileDropListHasInvalidFileDropPath"; + /// '{0}' must contain at least one file drop path. + internal const string @DataObject_FileDropListIsEmpty = "DataObject_FileDropListIsEmpty"; + /// '{0}' dwDirection parameter value is not supported. + internal const string @DataObject_NotImplementedEnumFormatEtc = "DataObject_NotImplementedEnumFormatEtc"; + /// Decompression of packet data failed. + internal const string @DecompressPacketDataFailed = "DecompressPacketDataFailed"; + /// Decompression of property data failed. + internal const string @DecompressPropertyFailed = "DecompressPropertyFailed"; + /// + internal const string @DecreaseZoomKey = "DecreaseZoomKey"; + /// + internal const string @DecreaseZoomKeyDisplayString = "DecreaseZoomKeyDisplayString"; + /// Decrease Zoom + internal const string @DecreaseZoomText = "DecreaseZoomText"; + /// Del + internal const string @DeleteKeyDisplayString = "DeleteKeyDisplayString"; + /// Delete + internal const string @DeleteText = "DeleteText"; + /// Cannot find a part of the path '{0}'. + internal const string @DirectoryNotFoundExceptionWithFileName = "DirectoryNotFoundExceptionWithFileName"; + /// '{0}' DragAction is not valid. + internal const string @DragDrop_DragActionInvalid = "DragDrop_DragActionInvalid"; + /// '{0}' DragDropEffects is not valid. + internal const string @DragDrop_DragDropEffectsInvalid = "DragDrop_DragDropEffectsInvalid"; + /// This Pop operation has no corresponding Push to remove from the stack because the stack depth of the DrawingContext is zero. + internal const string @DrawingContext_TooManyPops = "DrawingContext_TooManyPops"; + /// This object has an outstanding DrawingContext. The DrawingContext must be Closed or Disposed before making Open or Append calls. + internal const string @DrawingGroup_AlreadyOpen = "DrawingGroup_AlreadyOpen"; + /// Cannot append to a frozen DrawingGroup.Children collection. + internal const string @DrawingGroup_CannotAppendToFrozenCollection = "DrawingGroup_CannotAppendToFrozenCollection"; + /// Cannot append to a null DrawingGroup.Children collection. + internal const string @DrawingGroup_CannotAppendToNullCollection = "DrawingGroup_CannotAppendToNullCollection"; + /// Duplicate ApplicationGesture values are not allowed. + internal const string @DuplicateApplicationGestureFound = "DuplicateApplicationGestureFound"; + /// RoutedEvent Name '{0}' for OwnerType '{1}' already used. + internal const string @DuplicateEventName = "DuplicateEventName"; + /// Duplicate Stroke in StrokeCollectionChangedEventArgs.Added. + internal const string @DuplicateStrokeAdded = "DuplicateStrokeAdded"; + /// A pixel shader using Pixel Shader Model 2.0 cannot be set because registers only available in Pixel Shader Model 3.0 are being used. + internal const string @Effect_20ShaderUsing30Registers = "Effect_20ShaderUsing30Registers"; + /// BitmapEffect and Effect cannot be combined on a Visual. + internal const string @Effect_CombinedLegacyAndNew = "Effect_CombinedLegacyAndNew"; + /// Cannot call BitmapEffect.GetOutput directly with a ContextInputSource. Provide a valid BitmapSource. + internal const string @Effect_No_ContextInputSource = "Effect_No_ContextInputSource"; + /// There is no input set. + internal const string @Effect_No_InputSource = "Effect_No_InputSource"; + /// '{0}' PixelFormat is not supported for this operation. + internal const string @Effect_PixelFormat = "Effect_PixelFormat"; + /// An error occurred on the render thread with a user-supplied shader. + internal const string @Effect_RenderThreadError = "Effect_RenderThreadError"; + /// Pixel Shader Model 2.0 requires floating point constants to be in registers [0-31]. + internal const string @Effect_Shader20ConstantRegisterLimit = "Effect_Shader20ConstantRegisterLimit"; + /// Pixel Shader Model 2.0 requires sampler constants to be in registers [0-3]. + internal const string @Effect_Shader20SamplerRegisterLimit = "Effect_Shader20SamplerRegisterLimit"; + /// Pixel Shader Model 3.0 requires boolean constants to be in registers [0-15]. + internal const string @Effect_Shader30BoolConstantRegisterLimit = "Effect_Shader30BoolConstantRegisterLimit"; + /// Pixel Shader Model 3.0 requires floating point constants to be in registers [0-223]. + internal const string @Effect_Shader30FloatConstantRegisterLimit = "Effect_Shader30FloatConstantRegisterLimit"; + /// Pixel Shader Model 3.0 requires integer constants to be in registers [0-15]. + internal const string @Effect_Shader30IntConstantRegisterLimit = "Effect_Shader30IntConstantRegisterLimit"; + /// Pixel Shader Model 3.0 requires sampler constants to be in registers [0-7]. + internal const string @Effect_Shader30SamplerRegisterLimit = "Effect_Shader30SamplerRegisterLimit"; + /// Shader bytecode must be an integral number of 4-byte words. + internal const string @Effect_ShaderBytecodeSize = "Effect_ShaderBytecodeSize"; + /// No shader bytecode present. Must either set UriSource or call SetStreamSource. + internal const string @Effect_ShaderBytecodeSource = "Effect_ShaderBytecodeSource"; + /// Shader constant of type '{0}' is not allowed. + internal const string @Effect_ShaderConstantType = "Effect_ShaderConstantType"; + /// Padding must be non-negative. + internal const string @Effect_ShaderEffectPadding = "Effect_ShaderEffectPadding"; + /// PixelShader must be set on ShaderEffect. + internal const string @Effect_ShaderPixelShaderSet = "Effect_ShaderPixelShaderSet"; + /// Pixel shader sampler must be Effect.ImplicitInput, or an instance of BitmapCacheBrush, VisualBrush, or ImageBrush. + internal const string @Effect_ShaderSamplerType = "Effect_ShaderSamplerType"; + /// PixelShader only accepts seekable streams. + internal const string @Effect_ShaderSeekableStream = "Effect_ShaderSeekableStream"; + /// Uri must be a file or pack Uri. + internal const string @Effect_SourceUriMustBeFileOrPack = "Effect_SourceUriMustBeFileOrPack"; + /// Empty arrays are not a valid argument value. + internal const string @EmptyArray = "EmptyArray"; + /// The array cannot be empty. + internal const string @EmptyArrayNotAllowedAsArgument = "EmptyArrayNotAllowedAsArgument"; + /// No data to load. + internal const string @EmptyDataToLoad = "EmptyDataToLoad"; + /// Collection cannot be empty. + internal const string @EmptyScToReplace = "EmptyScToReplace"; + /// The replacement StrokeCollection cannot be empty. + internal const string @EmptyScToReplaceWith = "EmptyScToReplaceWith"; + /// EndHitTesting has already been called on the IncrementalHitTester. + internal const string @EndHitTestingCalled = "EndHitTestingCalled"; + /// End of stream reached. + internal const string @EndOfStreamReached = "EndOfStreamReached"; + /// The enumerator is not valid because the collection changed. + internal const string @Enumerator_CollectionChanged = "Enumerator_CollectionChanged"; + /// The enumerator has not been started. + internal const string @Enumerator_NotStarted = "Enumerator_NotStarted"; + /// The enumerator has reached the end of the collection. + internal const string @Enumerator_ReachedEnd = "Enumerator_ReachedEnd"; + /// No current object to return. + internal const string @Enumerator_VerifyContext = "Enumerator_VerifyContext"; + /// Text formatting engine cannot enumerate contents in a line due to error: '{0}'. + internal const string @EnumLineFailure = "EnumLineFailure"; + /// '{0}' enumeration value is not valid. + internal const string @Enum_Invalid = "Enum_Invalid"; + /// ExtendedProperty is already part of the ExtendedPropertyCollection. + internal const string @EPExists = "EPExists"; + /// The GUID is not part of the ExtendedPropertyCollection. + internal const string @EPGuidNotFound = "EPGuidNotFound"; + /// Property not set. + internal const string @EPNotFound = "EPNotFound"; + /// Event arguments must be non-null. + internal const string @EventArgIsNull = "EventArgIsNull"; + /// Shift+Down + internal const string @ExtendSelectionDownKeyDisplayString = "ExtendSelectionDownKeyDisplayString"; + /// Extend Selection Down + internal const string @ExtendSelectionDownText = "ExtendSelectionDownText"; + /// Shift+Left + internal const string @ExtendSelectionLeftKeyDisplayString = "ExtendSelectionLeftKeyDisplayString"; + /// Extend Selection Left + internal const string @ExtendSelectionLeftText = "ExtendSelectionLeftText"; + /// Shift+Right + internal const string @ExtendSelectionRightKeyDisplayString = "ExtendSelectionRightKeyDisplayString"; + /// Extend Selection Right + internal const string @ExtendSelectionRightText = "ExtendSelectionRightText"; + /// Shift+Up + internal const string @ExtendSelectionUpKeyDisplayString = "ExtendSelectionUpKeyDisplayString"; + /// Extend Selection Up + internal const string @ExtendSelectionUpText = "ExtendSelectionUpText"; + /// Font face index must be greater than or equal to zero. + internal const string @FaceIndexMustBePositiveOrZero = "FaceIndexMustBePositiveOrZero"; + /// Nonzero font face index values are valid only for TrueType collections (.ttc). + internal const string @FaceIndexValidOnlyForTTC = "FaceIndexValidOnlyForTTC"; + /// Cannot load system composite fonts. Location not found. + internal const string @FamilyCollection_CannotFindCompositeFontsLocation = "FamilyCollection_CannotFindCompositeFontsLocation"; + /// Cannot add FamilyMap because Target property is not set. + internal const string @FamilyMap_TargetNotSet = "FamilyMap_TargetNotSet"; + /// Ctrl+I + internal const string @FavoritesKeyDisplayString = "FavoritesKeyDisplayString"; + /// Favorites + internal const string @FavoritesText = "FavoritesText"; + /// Input file or data stream does not conform to the expected file format specification. + internal const string @FileFormatException = "FileFormatException"; + /// '{0}' file does not conform to the expected file format specification. + internal const string @FileFormatExceptionWithFileName = "FileFormatExceptionWithFileName"; + /// Cannot find file '{0}'. + internal const string @FileNotFoundExceptionWithFileName = "FileNotFoundExceptionWithFileName"; + /// Ctrl+F + internal const string @FindKeyDisplayString = "FindKeyDisplayString"; + /// Find + internal const string @FindText = "FindText"; + /// + internal const string @FirstPageKey = "FirstPageKey"; + /// + internal const string @FirstPageKeyDisplayString = "FirstPageKeyDisplayString"; + /// First Page + internal const string @FirstPageText = "FirstPageText"; + /// Unrecognized float type in BAML file. + internal const string @FloatUnknownBamlType = "FloatUnknownBamlType"; + /// Stream does not support Flush. + internal const string @FlushNotSupported = "FlushNotSupported"; + /// A named FontFamily object cannot be modified. + internal const string @FontFamily_ReadOnly = "FontFamily_ReadOnly"; + /// Specified value of type '{0}' must have IsFrozen set to false to modify. + internal const string @Freezable_CantBeFrozen = "Freezable_CantBeFrozen"; + /// Clone of an instance of type '{0}' is null or not an instance of '{0}'. + internal const string @Freezable_CloneInvalidType = "Freezable_CloneInvalidType"; + /// Cannot change FreezableCollection during a CollectionChanged event. + internal const string @Freezable_Reentrant = "Freezable_Reentrant"; + /// Unknown/unexpected change event + internal const string @Freezable_UnexpectedChange = "Freezable_UnexpectedChange"; + /// The transform is not defined for the point. + internal const string @GeneralTransform_TransformFailed = "GeneralTransform_TransformFailed"; + /// The object passed to '{0}' is not a valid type. + internal const string @General_BadType = "General_BadType"; + /// Expected object of type '{0}'. + internal const string @General_Expected_Type = "General_Expected_Type"; + /// The object is marked 'Read Only'. + internal const string @General_ObjectIsReadOnly = "General_ObjectIsReadOnly"; + /// Arithmetic error found while trying to perform this operation. + internal const string @Geometry_BadNumber = "Geometry_BadNumber"; + /// No gesture recognizer is available on the system. + internal const string @GestureRecognizerNotAvailable = "GestureRecognizerNotAvailable"; + /// Text formatting engine cannot retrieve penalty module handle due to error: '{0}'. + internal const string @GetPenaltyModuleHandleFailure = "GetPenaltyModuleHandleFailure"; + /// Cannot get response for web request to '{0}'. + internal const string @GetResponseFailed = "GetResponseFailed"; + /// Values for advanceWidths and glyphOffsets constitute too large of a GlyphRun. The area of its bounding box, measured in renderingEmSize squares, is '{0}' but it cannot exceed '{1}'. + internal const string @GlyphAreaTooBig = "GlyphAreaTooBig"; + /// advanceWidths and glyphOffsets constitute coordinate too large for glyph at index '{0}'. For renderingEmSize '{1}' the values cannot exceed '{2}'. + internal const string @GlyphCoordinateTooBig = "GlyphCoordinateTooBig"; + /// '{0}' glyph index is not valid for the specified font. + internal const string @GlyphIndexOutOfRange = "GlyphIndexOutOfRange"; + /// Glyph typeface URI does not point to a previously recorded glyph typeface. + internal const string @GlyphTypefaceNotRecorded = "GlyphTypefaceNotRecorded"; + /// + internal const string @GoToPageKey = "GoToPageKey"; + /// + internal const string @GoToPageKeyDisplayString = "GoToPageKeyDisplayString"; + /// Go To Page + internal const string @GoToPageText = "GoToPageText"; + /// Handler type is mismatched. + internal const string @HandlerTypeIllegal = "HandlerTypeIllegal"; + /// F1 + internal const string @HelpKeyDisplayString = "HelpKeyDisplayString"; + /// Help + internal const string @HelpText = "HelpText"; + /// '{0}' HitTestParameters are not supported on '{1}'. + internal const string @HitTest_Invalid = "HitTest_Invalid"; + /// Hit testing with a singular MatrixCamera is not supported. + internal const string @HitTest_Singular = "HitTest_Singular"; + /// Cannot access a disposed HwndSource. + internal const string @HwndSourceDisposed = "HwndSourceDisposed"; + /// Due to protocol mismatch hardware support is not available. + internal const string @HwndTarget_HardwareNotSupportDueToProtocolMismatch = "HwndTarget_HardwareNotSupportDueToProtocolMismatch"; + /// The specified handle is not a valid window handle. + internal const string @HwndTarget_InvalidWindowHandle = "HwndTarget_InvalidWindowHandle"; + /// The specified window does not belong to the current process. + internal const string @HwndTarget_InvalidWindowProcess = "HwndTarget_InvalidWindowProcess"; + /// The specified window does not belong to the current thread. + internal const string @HwndTarget_InvalidWindowThread = "HwndTarget_InvalidWindowThread"; + /// Another HwndTarget is associated with this window. + internal const string @HwndTarget_WindowAlreadyHasContent = "HwndTarget_WindowAlreadyHasContent"; + /// Cannot animate the '{0}' property on '{1}' because the object is sealed or frozen. + internal const string @IAnimatable_CantAnimateSealedDO = "IAnimatable_CantAnimateSealedDO"; + /// Alpha threshold must be from 0 through 100. + internal const string @Image_AlphaThresholdOutOfRange = "Image_AlphaThresholdOutOfRange"; + /// The bitmap specified does not have the correct dimensions. + internal const string @Image_BadDimensions = "Image_BadDimensions"; + /// The image has corrupted metadata header. + internal const string @Image_BadMetadataHeader = "Image_BadMetadataHeader"; + /// '{0}' not a valid pixel format. + internal const string @Image_BadPixelFormat = "Image_BadPixelFormat"; + /// The stream is corrupted. + internal const string @Image_BadStreamData = "Image_BadStreamData"; + /// DLL version not correct. + internal const string @Image_BadVersion = "Image_BadVersion"; + /// Unable to create temporary file for download. + internal const string @Image_CannotCreateTempFile = "Image_CannotCreateTempFile"; + /// The Image passed to the ImageVisualManager cannot be frozen. + internal const string @Image_CantBeFrozen = "Image_CantBeFrozen"; + /// The codec cannot use the type of stream provided. + internal const string @Image_CantDealWithStream = "Image_CantDealWithStream"; + /// The codec cannot use the type of URI provided. + internal const string @Image_CantDealWithUri = "Image_CantDealWithUri"; + /// Codec added more than once. + internal const string @Image_CodecPresent = "Image_CodecPresent"; + /// Color context is not valid. + internal const string @Image_ColorContextInvalid = "Image_ColorContextInvalid"; + /// Color transform is not valid. + internal const string @Image_ColorTransformInvalid = "Image_ColorTransformInvalid"; + /// No imaging component suitable to complete this operation was found. + internal const string @Image_ComponentNotFound = "Image_ComponentNotFound"; + /// The mime type registered with the system does not match the mime type of the file. + internal const string @Image_ContentTypeDoesNotMatchDecoder = "Image_ContentTypeDoesNotMatchDecoder"; + /// The image decoder cannot decode the image. The image might be corrupted. + internal const string @Image_DecoderError = "Image_DecoderError"; + /// The system display state is not valid. + internal const string @Image_DisplayStateInvalid = "Image_DisplayStateInvalid"; + /// Duplicate copies of metadata present. + internal const string @Image_DuplicateMetadataPresent = "Image_DuplicateMetadataPresent"; + /// The designated BitmapEncoder does not support ColorContexts. + internal const string @Image_EncoderNoColorContext = "Image_EncoderNoColorContext"; + /// The designated BitmapEncoder does not support global metadata. + internal const string @Image_EncoderNoGlobalMetadata = "Image_EncoderNoGlobalMetadata"; + /// The designated BitmapEncoder does not support global thumbnails. + internal const string @Image_EncoderNoGlobalThumbnail = "Image_EncoderNoGlobalThumbnail"; + /// The designated BitmapEncoder does not support previews. + internal const string @Image_EncoderNoPreview = "Image_EncoderNoPreview"; + /// Cannot call EndInit without a matching BeginInit call. + internal const string @Image_EndInitWithoutBeginInit = "Image_EndInitWithoutBeginInit"; + /// The image is missing a frame. + internal const string @Image_FrameMissing = "Image_FrameMissing"; + /// This class does not support cloning. + internal const string @Image_FreezableCloneNotAllowed = "Image_FreezableCloneNotAllowed"; + /// Empty GUID is not valid for '{0}'. + internal const string @Image_GuidEmpty = "Image_GuidEmpty"; + /// The image cannot be decoded. The image header might be corrupted. + internal const string @Image_HeaderError = "Image_HeaderError"; + /// Must specify a palette when using an indexed pixel format. + internal const string @Image_IndexedPixelFormatRequiresPalette = "Image_IndexedPixelFormatRequiresPalette"; + /// Already in an initializing state. + internal const string @Image_InInitialize = "Image_InInitialize"; + /// BitmapImage initialization is not complete. Call the EndInit method to complete the initialization. + internal const string @Image_InitializationIncomplete = "Image_InitializationIncomplete"; + /// InPlaceBitmapMetadataWriter cannot be copied. + internal const string @Image_InplaceMetadataNoCopy = "Image_InplaceMetadataNoCopy"; + /// Buffer size is not sufficient. + internal const string @Image_InsufficientBuffer = "Image_InsufficientBuffer"; + /// Buffer not large enough to copy memory. + internal const string @Image_InsufficientBufferSize = "Image_InsufficientBufferSize"; + /// An error occurred. + internal const string @Image_InternalError = "Image_InternalError"; + /// Cannot match the type of this array to a pixel format. + internal const string @Image_InvalidArrayForPixel = "Image_InvalidArrayForPixel"; + /// Bitmap color context is not valid. + internal const string @Image_InvalidColorContext = "Image_InvalidColorContext"; + /// Character is not valid in metadata query request. + internal const string @Image_InvalidQueryCharacter = "Image_InvalidQueryCharacter"; + /// Metadata query request is not valid. + internal const string @Image_InvalidQueryRequest = "Image_InvalidQueryRequest"; + /// The lock count cannot exceed UInt32.MaxValue. + internal const string @Image_LockCountLimit = "Image_LockCountLimit"; + /// BitmapMetadata initialization incomplete. + internal const string @Image_MetadataInitializationIncomplete = "Image_MetadataInitializationIncomplete"; + /// The bitmap metadata is not compatible with this container format. + internal const string @Image_MetadataNotCompatible = "Image_MetadataNotCompatible"; + /// BitmapMetadata is not available on BitmapImage. + internal const string @Image_MetadataNotSupported = "Image_MetadataNotSupported"; + /// Bitmap metadata cannot be changed. + internal const string @Image_MetadataReadOnly = "Image_MetadataReadOnly"; + /// Cannot add any more top-level metadata blocks. + internal const string @Image_MetadataSizeFixed = "Image_MetadataSizeFixed"; + /// Cannot call this method while the image is unlocked. + internal const string @Image_MustBeLocked = "Image_MustBeLocked"; + /// Property '{0}' or property '{1}' must be set. + internal const string @Image_NeitherArgument = "Image_NeitherArgument"; + /// '{0}' property is not set. + internal const string @Image_NoArgument = "Image_NoArgument"; + /// No codec found that can decode the specified file. + internal const string @Image_NoCodecsFound = "Image_NoCodecsFound"; + /// Image does not contain any frames. + internal const string @Image_NoDecodeFrames = "Image_NoDecodeFrames"; + /// Cannot save an image with no frames. + internal const string @Image_NoFrames = "Image_NoFrames"; + /// The specified image does not contain a palette. + internal const string @Image_NoPalette = "Image_NoPalette"; + /// No information was found about this pixel format. + internal const string @Image_NoPixelFormatFound = "Image_NoPixelFormatFound"; + /// Bitmap does not contain thumbnail. + internal const string @Image_NoThumbnail = "Image_NoThumbnail"; + /// BitmapImage has not been initialized. Call the BeginInit method, set the appropriate properties, and then call the EndInit method. + internal const string @Image_NotInitialized = "Image_NotInitialized"; + /// Cannot set the initializing state more than once. + internal const string @Image_OnlyOneInit = "Image_OnlyOneInit"; + /// Cannot call Save on an Encoder more than once. + internal const string @Image_OnlyOneSave = "Image_OnlyOneSave"; + /// Transform must be a combination of scales, flips, and 90 degree rotations. + internal const string @Image_OnlyOrthogonal = "Image_OnlyOrthogonal"; + /// In place editing of bitmap metadata is not allowed because the original source is not writable. + internal const string @Image_OriginalStreamReadOnly = "Image_OriginalStreamReadOnly"; + /// The image data generated an overflow during processing. + internal const string @Image_Overflow = "Image_Overflow"; + /// The number of colors in the palette is larger than the maximum allowed by the supplied pixel format. + internal const string @Image_PaletteColorsDoNotMatchFormat = "Image_PaletteColorsDoNotMatchFormat"; + /// Must use a fixed palette type. '{0}' not supported here. + internal const string @Image_PaletteFixedType = "Image_PaletteFixedType"; + /// Cannot create a palette with less than 1 color or more than 256 colors. + internal const string @Image_PaletteZeroColors = "Image_PaletteZeroColors"; + /// Property cannot be found. + internal const string @Image_PropertyNotFound = "Image_PropertyNotFound"; + /// This codec does not support the specified property. + internal const string @Image_PropertyNotSupported = "Image_PropertyNotSupported"; + /// Property is corrupted. + internal const string @Image_PropertySize = "Image_PropertySize"; + /// Unexpected property type or value. + internal const string @Image_PropertyUnexpectedType = "Image_PropertyUnexpectedType"; + /// The metadata query is valid only at the root of the metadata hierarchy. + internal const string @Image_RequestOnlyValidAtMetadataRoot = "Image_RequestOnlyValidAtMetadataRoot"; + /// Cannot set this property outside a BeginInit/EndInit block. + internal const string @Image_SetPropertyOutsideBeginEndInit = "Image_SetPropertyOutsideBeginEndInit"; + /// Cannot invert singular matrix. + internal const string @Image_SingularMatrix = "Image_SingularMatrix"; + /// Bad Rotation parameter. Only Rotate0, Rotate90, Rotate180, and Rotate270 are supported. + internal const string @Image_SizeOptionsAngle = "Image_SizeOptionsAngle"; + /// The image dimensions are out of the range supported by this codec. + internal const string @Image_SizeOutOfRange = "Image_SizeOutOfRange"; + /// Metadata stream is not available for this operation. + internal const string @Image_StreamNotAvailable = "Image_StreamNotAvailable"; + /// Cannot read from the stream. + internal const string @Image_StreamRead = "Image_StreamRead"; + /// Cannot write to the stream. + internal const string @Image_StreamWrite = "Image_StreamWrite"; + /// The bitmap has too many scanlines for the specified encoder. + internal const string @Image_TooManyScanlines = "Image_TooManyScanlines"; + /// Commit unsuccessful because too much metadata changed. + internal const string @Image_TooMuchMetadata = "Image_TooMuchMetadata"; + /// Unexpected type of metadata. + internal const string @Image_UnexpectedMetadataType = "Image_UnexpectedMetadataType"; + /// The image format is unrecognized. + internal const string @Image_UnknownFormat = "Image_UnknownFormat"; + /// Operation not supported. + internal const string @Image_UnsupportedOperation = "Image_UnsupportedOperation"; + /// Pixel format not supported. + internal const string @Image_UnsupportedPixelFormat = "Image_UnsupportedPixelFormat"; + /// Operation caused an invalid state. + internal const string @Image_WrongState = "Image_WrongState"; + /// The StylusPointDescriptions are incompatible. Use the StylusPointDescription.GetCommonDescription method to find a common StylusPointDescription and then call StylusPointCollection.Reformat to return a compatible StylusPointCollection. + internal const string @IncompatibleStylusPointDescriptions = "IncompatibleStylusPointDescriptions"; + /// + internal const string @IncreaseZoomKey = "IncreaseZoomKey"; + /// + internal const string @IncreaseZoomKeyDisplayString = "IncreaseZoomKeyDisplayString"; + /// Increase Zoom + internal const string @IncreaseZoomText = "IncreaseZoomText"; + /// The object is already being initialized. + internal const string @InInitialization = "InInitialization"; + /// The operation fails because the object is not fully initialized. + internal const string @InitializationIncomplete = "InitializationIncomplete"; + /// Cannot initialize compressor. + internal const string @InitializingCompressorFailed = "InitializingCompressorFailed"; + /// InnerRequest not available for preloaded packages. + internal const string @InnerRequestNotAllowed = "InnerRequestNotAllowed"; + /// Gesture accepts only objects of type '{0}'. + internal const string @InputBinding_ExpectedInputGesture = "InputBinding_ExpectedInputGesture"; + /// InputLanguageManager is not ready to change the current input languages. + internal const string @InputLanguageManager_NotReadyToChangeCurrentLanguage = "InputLanguageManager_NotReadyToChangeCurrentLanguage"; + /// '{0}' is not a valid ImeConversionMode. + internal const string @InputMethod_InvalidConversionMode = "InputMethod_InvalidConversionMode"; + /// '{0}' is not a valid ImeSentenceMode. + internal const string @InputMethod_InvalidSentenceMode = "InputMethod_InvalidSentenceMode"; + /// The InputProviderSite has already been disposed. + internal const string @InputProviderSiteDisposed = "InputProviderSiteDisposed"; + /// '{0}' is not a valid InputScopeName. + internal const string @InputScope_InvalidInputScopeName = "InputScope_InvalidInputScopeName"; + /// Integer collection size cannot be negative. + internal const string @IntegerCollectionLengthLessThanZero = "IntegerCollectionLengthLessThanZero"; + /// An absolute URI in a font family name must have file:// scheme. + internal const string @InvalidAbsoluteUriInFontFamilyName = "InvalidAbsoluteUriInFontFamilyName"; + /// The additional data passed in does not match what is expected based on the StylusPointDescription. + internal const string @InvalidAdditionalDataForStylusPoint = "InvalidAdditionalDataForStylusPoint"; + /// Maximum buffer length must be within actual buffer length. + internal const string @InvalidBufferLength = "InvalidBufferLength"; + /// Byte ranges are not valid in '{0}'. + internal const string @InvalidByteRanges = "InvalidByteRanges"; + /// '{0}' cursor type is not valid. + internal const string @InvalidCursorType = "InvalidCursorType"; + /// The specified data is invalid, see inner exception for details. + internal const string @InvalidDataInISF = "InvalidDataInISF"; + /// Property data must be a non-reference variant compatible type. + internal const string @InvalidDataTypeForExtendedProperty = "InvalidDataTypeForExtendedProperty"; + /// The value is out of range. + internal const string @InvalidDiameter = "InvalidDiameter"; + /// Height must be greater than or equal to DrawingAttributes.MinHeight and less than or equal to DrawingAttribute.MaxHeight. + internal const string @InvalidDrawingAttributesHeight = "InvalidDrawingAttributesHeight"; + /// Width must be greater than or equal to DrawingAttributes.MinWidth and less than or equal to DrawingAttribute.MaxWidth. + internal const string @InvalidDrawingAttributesWidth = "InvalidDrawingAttributesWidth"; + /// Extended property data type is not valid. + internal const string @InvalidEpInIsf = "InvalidEpInIsf"; + /// The event handle is not usable. + internal const string @InvalidEventHandle = "InvalidEventHandle"; + /// GUID cannot be empty. + internal const string @InvalidGuid = "InvalidGuid"; + /// The specified GUID represents a button, so isButton must be true. + internal const string @InvalidIsButtonForId = "InvalidIsButtonForId"; + /// The specified GUID does not represent a button, so isButton must be false. + internal const string @InvalidIsButtonForId2 = "InvalidIsButtonForId2"; + /// Infinity member value is not valid in Matrix. + internal const string @InvalidMatrixContainsInfinity = "InvalidMatrixContainsInfinity"; + /// NaN is not a valid value for Matrix member. + internal const string @InvalidMatrixContainsNaN = "InvalidMatrixContainsNaN"; + /// StylusPointPropertyInfos that are buttons must have a minimum of 0 and a maximum of 1. + internal const string @InvalidMinMaxForButton = "InvalidMinMaxForButton"; + /// The part name does not correspond to its content type. + internal const string @InvalidPartName = "InvalidPartName"; + /// PermissionState value '{0}' is not valid for this Permission. + internal const string @InvalidPermissionStateValue = "InvalidPermissionStateValue"; + /// Permission type is not valid. Expected '{0}'. + internal const string @InvalidPermissionType = "InvalidPermissionType"; + /// Pressure must be a value between 0 and 1. + internal const string @InvalidPressureValue = "InvalidPressureValue"; + /// The stroke being removed does not exist in the current collection. + internal const string @InvalidRemovedStroke = "InvalidRemovedStroke"; + /// The stroke being replaced does not exist in the current collection. + internal const string @InvalidReplacedStroke = "InvalidReplacedStroke"; + /// HTTP byte range downloader can support only HTTP or HTTPS schemes. + internal const string @InvalidScheme = "InvalidScheme"; + /// '{0}' must be a relative URI for site of origin. + internal const string @InvalidSiteOfOriginUri = "InvalidSiteOfOriginUri"; + /// The specified size is less than the information decoded in the ISF stream. + internal const string @InvalidSizeSpecified = "InvalidSizeSpecified"; + /// Stream is not valid. + internal const string @InvalidStream = "InvalidStream"; + /// Translation is not valid. + internal const string @InvalidSttValue = "InvalidSttValue"; + /// StylusPointCollection cannot be empty when attached to a Stroke. + internal const string @InvalidStylusPointCollectionZeroCount = "InvalidStylusPointCollectionZeroCount"; + /// The specified collection cannot be empty. + internal const string @InvalidStylusPointConstructionZeroLengthCollection = "InvalidStylusPointConstructionZeroLengthCollection"; + /// StylusPointDescription must contain at least X, Y and NormalPressure in that order. + internal const string @InvalidStylusPointDescription = "InvalidStylusPointDescription"; + /// When constructing a StylusPointDescription, any StylusPointPropertyInfos that represent buttons must be placed at the end of the collection. + internal const string @InvalidStylusPointDescriptionButtonsMustBeLast = "InvalidStylusPointDescriptionButtonsMustBeLast"; + /// StylusPointDescription cannot contain duplicate StylusPointPropertyInfos. + internal const string @InvalidStylusPointDescriptionDuplicatesFound = "InvalidStylusPointDescriptionDuplicatesFound"; + /// The specified StylusPointDescription must be a subset. + internal const string @InvalidStylusPointDescriptionSubset = "InvalidStylusPointDescriptionSubset"; + /// StylusPointDescription supports no more than 31 buttons. + internal const string @InvalidStylusPointDescriptionTooManyButtons = "InvalidStylusPointDescriptionTooManyButtons"; + /// The StylusPoint does not support the specified StylusPointProperty. + internal const string @InvalidStylusPointProperty = "InvalidStylusPointProperty"; + /// Resolution must be at least 0.0f. + internal const string @InvalidStylusPointPropertyInfoResolution = "InvalidStylusPointPropertyInfoResolution"; + /// Value cannot be Double.NaN. + internal const string @InvalidStylusPointXYNaN = "InvalidStylusPointXYNaN"; + /// Cannot have empty name of a temporary file. + internal const string @InvalidTempFileName = "InvalidTempFileName"; + /// The requested TextDecorationCollection string is not valid: '{0}'. + internal const string @InvalidTextDecorationCollectionString = "InvalidTextDecorationCollectionString"; + /// Invalid value '{0}' for type '{1}'. + internal const string @InvalidValueOfType = "InvalidValueOfType"; + /// Value must be of type '{0}'. + internal const string @InvalidValueType = "InvalidValueType"; + /// Value must be of type '{0}' or '{1}'. + internal const string @InvalidValueType1 = "InvalidValueType1"; + /// '{0}' is not a valid type for IInputElement. UIElement or ContentElement expected. + internal const string @Invalid_IInputElement = "Invalid_IInputElement"; + /// The length of the ISF data must be greater than zero. + internal const string @Invalid_isfData_Length = "Invalid_isfData_Length"; + /// The URI specified is invalid. + internal const string @Invalid_URI = "Invalid_URI"; + /// A read or write operation references a location outside the bounds of the buffer provided. + internal const string @IOBufferOverflow = "IOBufferOverflow"; + /// I/O error when opening file '{0}'. + internal const string @IOExceptionWithFileName = "IOExceptionWithFileName"; + /// InkSerializedFormat operation failed. + internal const string @IsfOperationFailed = "IsfOperationFailed"; + /// The specified keyboard sink is already owned by a site. + internal const string @KeyboardSinkAlreadyOwned = "KeyboardSinkAlreadyOwned"; + /// The specified keyboard sink must be a UIElement. + internal const string @KeyboardSinkMustBeAnElement = "KeyboardSinkMustBeAnElement"; + /// The specified keyboard sink is not a child of this source. + internal const string @KeyboardSinkNotAChild = "KeyboardSinkNotAChild"; + /// '{0}+{1}' key and modifier combination is not supported for KeyGesture. + internal const string @KeyGesture_Invalid = "KeyGesture_Invalid"; + /// + internal const string @LastPageKey = "LastPageKey"; + /// + internal const string @LastPageKeyDisplayString = "LastPageKeyDisplayString"; + /// Last Page + internal const string @LastPageText = "LastPageText"; + /// Layout recursion reached allowed limit to avoid stack overflow: '{0}'. Either the tree contains a loop or is too deep. + internal const string @LayoutManager_DeepRecursion = "LayoutManager_DeepRecursion"; + /// An unrecognized ManipulationMode flag was encountered. + internal const string @Manipulation_InvalidManipulationMode = "Manipulation_InvalidManipulationMode"; + /// Manipulation is not active on the specified element. + internal const string @Manipulation_ManipulationNotActive = "Manipulation_ManipulationNotActive"; + /// IsManipulationEnabled is not set to true on the specified element. + internal const string @Manipulation_ManipulationNotEnabled = "Manipulation_ManipulationNotEnabled"; + /// Cannot invert the matrix, because the matrix is not invertible. + internal const string @Matrix3D_NotInvertible = "Matrix3D_NotInvertible"; + /// The specified Matrix must be invertible. + internal const string @MatrixNotInvertible = "MatrixNotInvertible"; + /// + internal const string @MediaBoostBassKey = "MediaBoostBassKey"; + /// + internal const string @MediaBoostBassKeyDisplayString = "MediaBoostBassKeyDisplayString"; + /// Boost Bass + internal const string @MediaBoostBassText = "MediaBoostBassText"; + /// + internal const string @MediaChannelDownKey = "MediaChannelDownKey"; + /// + internal const string @MediaChannelDownKeyDisplayString = "MediaChannelDownKeyDisplayString"; + /// Channel Down + internal const string @MediaChannelDownText = "MediaChannelDownText"; + /// + internal const string @MediaChannelUpKey = "MediaChannelUpKey"; + /// + internal const string @MediaChannelUpKeyDisplayString = "MediaChannelUpKeyDisplayString"; + /// Channel Up + internal const string @MediaChannelUpText = "MediaChannelUpText"; + /// Cannot call this API during the OnRender callback. During OnRender, only drawing operations that draw the content of the Visual can be performed. + internal const string @MediaContext_APINotAllowed = "MediaContext_APINotAllowed"; + /// An infinite loop appears to have resulted from cross-dependent views. + internal const string @MediaContext_InfiniteLayoutLoop = "MediaContext_InfiniteLayoutLoop"; + /// An infinite loop appears to have resulted from repeatedly invalidating the TimeManager during the Layout/Render process. + internal const string @MediaContext_InfiniteTickLoop = "MediaContext_InfiniteTickLoop"; + /// Invalid user-specified pixel shader. Register a PixelShader.InvalidPixelShaderEncountered event handler to avoid this exception being raised. + internal const string @MediaContext_NoBadShaderHandler = "MediaContext_NoBadShaderHandler"; + /// Out of video memory. + internal const string @MediaContext_OutOfVideoMemory = "MediaContext_OutOfVideoMemory"; + /// An unspecified error occurred on the render thread. + internal const string @MediaContext_RenderThreadError = "MediaContext_RenderThreadError"; + /// + internal const string @MediaDecreaseBassKey = "MediaDecreaseBassKey"; + /// + internal const string @MediaDecreaseBassKeyDisplayString = "MediaDecreaseBassKeyDisplayString"; + /// Decrease Bass + internal const string @MediaDecreaseBassText = "MediaDecreaseBassText"; + /// + internal const string @MediaDecreaseMicrophoneVolumeKey = "MediaDecreaseMicrophoneVolumeKey"; + /// + internal const string @MediaDecreaseMicrophoneVolumeKeyDisplayString = "MediaDecreaseMicrophoneVolumeKeyDisplayString"; + /// Decrease Microphone Volume + internal const string @MediaDecreaseMicrophoneVolumeText = "MediaDecreaseMicrophoneVolumeText"; + /// + internal const string @MediaDecreaseTrebleKey = "MediaDecreaseTrebleKey"; + /// + internal const string @MediaDecreaseTrebleKeyDisplayString = "MediaDecreaseTrebleKeyDisplayString"; + /// Decrease Treble + internal const string @MediaDecreaseTrebleText = "MediaDecreaseTrebleText"; + /// + internal const string @MediaDecreaseVolumeKey = "MediaDecreaseVolumeKey"; + /// + internal const string @MediaDecreaseVolumeKeyDisplayString = "MediaDecreaseVolumeKeyDisplayString"; + /// Decrease Volume + internal const string @MediaDecreaseVolumeText = "MediaDecreaseVolumeText"; + /// + internal const string @MediaFastForwardKey = "MediaFastForwardKey"; + /// + internal const string @MediaFastForwardKeyDisplayString = "MediaFastForwardKeyDisplayString"; + /// Fast Forward + internal const string @MediaFastForwardText = "MediaFastForwardText"; + /// + internal const string @MediaIncreaseBassKey = "MediaIncreaseBassKey"; + /// + internal const string @MediaIncreaseBassKeyDisplayString = "MediaIncreaseBassKeyDisplayString"; + /// Increase Bass + internal const string @MediaIncreaseBassText = "MediaIncreaseBassText"; + /// + internal const string @MediaIncreaseMicrophoneVolumeKey = "MediaIncreaseMicrophoneVolumeKey"; + /// + internal const string @MediaIncreaseMicrophoneVolumeKeyDisplayString = "MediaIncreaseMicrophoneVolumeKeyDisplayString"; + /// Increase Microphone Volume + internal const string @MediaIncreaseMicrophoneVolumeText = "MediaIncreaseMicrophoneVolumeText"; + /// + internal const string @MediaIncreaseTrebleKey = "MediaIncreaseTrebleKey"; + /// + internal const string @MediaIncreaseTrebleKeyDisplayString = "MediaIncreaseTrebleKeyDisplayString"; + /// Increase Treble + internal const string @MediaIncreaseTrebleText = "MediaIncreaseTrebleText"; + /// + internal const string @MediaIncreaseVolumeKey = "MediaIncreaseVolumeKey"; + /// + internal const string @MediaIncreaseVolumeKeyDisplayString = "MediaIncreaseVolumeKeyDisplayString"; + /// Increase Volume + internal const string @MediaIncreaseVolumeText = "MediaIncreaseVolumeText"; + /// + internal const string @MediaMuteMicrophoneVolumeKey = "MediaMuteMicrophoneVolumeKey"; + /// + internal const string @MediaMuteMicrophoneVolumeKeyDisplayString = "MediaMuteMicrophoneVolumeKeyDisplayString"; + /// Mute Microphone Volume + internal const string @MediaMuteMicrophoneVolumeText = "MediaMuteMicrophoneVolumeText"; + /// + internal const string @MediaMuteVolumeKey = "MediaMuteVolumeKey"; + /// + internal const string @MediaMuteVolumeKeyDisplayString = "MediaMuteVolumeKeyDisplayString"; + /// Mute Volume + internal const string @MediaMuteVolumeText = "MediaMuteVolumeText"; + /// + internal const string @MediaNextTrackKey = "MediaNextTrackKey"; + /// + internal const string @MediaNextTrackKeyDisplayString = "MediaNextTrackKeyDisplayString"; + /// Next Track + internal const string @MediaNextTrackText = "MediaNextTrackText"; + /// + internal const string @MediaPauseKey = "MediaPauseKey"; + /// + internal const string @MediaPauseKeyDisplayString = "MediaPauseKeyDisplayString"; + /// Pause + internal const string @MediaPauseText = "MediaPauseText"; + /// + internal const string @MediaPlayKey = "MediaPlayKey"; + /// + internal const string @MediaPlayKeyDisplayString = "MediaPlayKeyDisplayString"; + /// Play + internal const string @MediaPlayText = "MediaPlayText"; + /// + internal const string @MediaPreviousTrackKey = "MediaPreviousTrackKey"; + /// + internal const string @MediaPreviousTrackKeyDisplayString = "MediaPreviousTrackKeyDisplayString"; + /// Previous Track + internal const string @MediaPreviousTrackText = "MediaPreviousTrackText"; + /// + internal const string @MediaRecordKey = "MediaRecordKey"; + /// + internal const string @MediaRecordKeyDisplayString = "MediaRecordKeyDisplayString"; + /// Record + internal const string @MediaRecordText = "MediaRecordText"; + /// + internal const string @MediaRewindKey = "MediaRewindKey"; + /// + internal const string @MediaRewindKeyDisplayString = "MediaRewindKeyDisplayString"; + /// Rewind + internal const string @MediaRewindText = "MediaRewindText"; + /// + internal const string @MediaSelectKey = "MediaSelectKey"; + /// + internal const string @MediaSelectKeyDisplayString = "MediaSelectKeyDisplayString"; + /// Select + internal const string @MediaSelectText = "MediaSelectText"; + /// + internal const string @MediaStopKey = "MediaStopKey"; + /// + internal const string @MediaStopKeyDisplayString = "MediaStopKeyDisplayString"; + /// Stop + internal const string @MediaStopText = "MediaStopText"; + /// This API was accessed with arguments from the wrong context. + internal const string @MediaSystem_ApiInvalidContext = "MediaSystem_ApiInvalidContext"; + /// Received an out of order connect or disconnect message. + internal const string @MediaSystem_OutOfOrderConnectOrDisconnect = "MediaSystem_OutOfOrderConnectOrDisconnect"; + /// + internal const string @MediaToggleMicrophoneOnOffKey = "MediaToggleMicrophoneOnOffKey"; + /// + internal const string @MediaToggleMicrophoneOnOffKeyDisplayString = "MediaToggleMicrophoneOnOffKeyDisplayString"; + /// Toggle Microphone OnOff + internal const string @MediaToggleMicrophoneOnOffText = "MediaToggleMicrophoneOnOffText"; + /// + internal const string @MediaTogglePlayPauseKey = "MediaTogglePlayPauseKey"; + /// + internal const string @MediaTogglePlayPauseKeyDisplayString = "MediaTogglePlayPauseKeyDisplayString"; + /// Toggle Play Pause + internal const string @MediaTogglePlayPauseText = "MediaTogglePlayPauseText"; + /// Media file download failed. + internal const string @Media_DownloadFailed = "Media_DownloadFailed"; + /// Installed codecs do not support the media file format. + internal const string @Media_FileFormatNotSupported = "Media_FileFormatNotSupported"; + /// Cannot find the media file. + internal const string @Media_FileNotFound = "Media_FileNotFound"; + /// Display driver must support video acceleration for video or audio playback. + internal const string @Media_HardwareVideoAccelerationNotAvailable = "Media_HardwareVideoAccelerationNotAvailable"; + /// There are insufficient video resources available for video or audio playback. + internal const string @Media_InsufficientVideoResources = "Media_InsufficientVideoResources"; + /// Value does not fall within the expected range. + internal const string @Media_InvalidArgument = "Media_InvalidArgument"; + /// Windows Media Player version 10 or later is required. + internal const string @Media_InvalidWmpVersion = "Media_InvalidWmpVersion"; + /// Access was denied on the media file. + internal const string @Media_LogonFailure = "Media_LogonFailure"; + /// Cannot perform this operation while a clock is assigned to the media player. + internal const string @Media_NotAllowedWhileTimingEngineInControl = "Media_NotAllowedWhileTimingEngineInControl"; + /// Only site-of-origin pack URIs are supported for media. + internal const string @Media_PackURIsAreNotSupported = "Media_PackURIsAreNotSupported"; + /// No operations are valid on a closed media player except open and close. + internal const string @Media_PlayerIsClosed = "Media_PlayerIsClosed"; + /// Unrecognized playlist file format. + internal const string @Media_PlaylistFormatNotSupported = "Media_PlaylistFormatNotSupported"; + /// Cannot access the stream after it is closed. + internal const string @Media_StreamClosed = "Media_StreamClosed"; + /// Accessed an uninitialized media resource. + internal const string @Media_UninitializedResource = "Media_UninitializedResource"; + /// Channel type is not recognized. + internal const string @Media_UnknownChannelType = "Media_UnknownChannelType"; + /// An unknown media error occurred. + internal const string @Media_UnknownMediaExecption = "Media_UnknownMediaExecption"; + /// Must specify URI. + internal const string @Media_UriNotSpecified = "Media_UriNotSpecified"; + /// The '{0}' method cannot be called at this time. + internal const string @MethodCallNotAllowed = "MethodCallNotAllowed"; + /// Mismatched versions of PresentationCore.dll, Milcore.dll, WindowsCodecs.dll, or D3d9.dll. Check that these DLLs come from the same source. + internal const string @MilErr_UnsupportedVersion = "MilErr_UnsupportedVersion"; + /// RoutedEvent in RoutedEventArgs and EventRoute are mismatched. + internal const string @Mismatched_RoutedEvent = "Mismatched_RoutedEvent"; + /// Down + internal const string @MoveDownKeyDisplayString = "MoveDownKeyDisplayString"; + /// Move Down + internal const string @MoveDownText = "MoveDownText"; + /// Ctrl+Left + internal const string @MoveFocusBackKeyDisplayString = "MoveFocusBackKeyDisplayString"; + /// Move Focus Back + internal const string @MoveFocusBackText = "MoveFocusBackText"; + /// Ctrl+Down + internal const string @MoveFocusDownKeyDisplayString = "MoveFocusDownKeyDisplayString"; + /// Move Focus Down + internal const string @MoveFocusDownText = "MoveFocusDownText"; + /// Ctrl+Right + internal const string @MoveFocusForwardKeyDisplayString = "MoveFocusForwardKeyDisplayString"; + /// Move Focus Forward + internal const string @MoveFocusForwardText = "MoveFocusForwardText"; + /// Ctrl+PageDown + internal const string @MoveFocusPageDownKeyDisplayString = "MoveFocusPageDownKeyDisplayString"; + /// Move Focus Page Down + internal const string @MoveFocusPageDownText = "MoveFocusPageDownText"; + /// Ctrl+PageUp + internal const string @MoveFocusPageUpKeyDisplayString = "MoveFocusPageUpKeyDisplayString"; + /// Move Focus Page Up + internal const string @MoveFocusPageUpText = "MoveFocusPageUpText"; + /// Ctrl+Up + internal const string @MoveFocusUpKeyDisplayString = "MoveFocusUpKeyDisplayString"; + /// Move Focus Up + internal const string @MoveFocusUpText = "MoveFocusUpText"; + /// Left + internal const string @MoveLeftKeyDisplayString = "MoveLeftKeyDisplayString"; + /// Move Left + internal const string @MoveLeftText = "MoveLeftText"; + /// Right + internal const string @MoveRightKeyDisplayString = "MoveRightKeyDisplayString"; + /// Move Right + internal const string @MoveRightText = "MoveRightText"; + /// End + internal const string @MoveToEndKeyDisplayString = "MoveToEndKeyDisplayString"; + /// Move To End + internal const string @MoveToEndText = "MoveToEndText"; + /// Home + internal const string @MoveToHomeKeyDisplayString = "MoveToHomeKeyDisplayString"; + /// Move To Home + internal const string @MoveToHomeText = "MoveToHomeText"; + /// PageDown + internal const string @MoveToPageDownKeyDisplayString = "MoveToPageDownKeyDisplayString"; + /// Move To Page Down + internal const string @MoveToPageDownText = "MoveToPageDownText"; + /// PageUp + internal const string @MoveToPageUpKeyDisplayString = "MoveToPageUpKeyDisplayString"; + /// Move To Page Up + internal const string @MoveToPageUpText = "MoveToPageUpText"; + /// Up + internal const string @MoveUpKeyDisplayString = "MoveUpKeyDisplayString"; + /// Move Up + internal const string @MoveUpText = "MoveUpText"; + /// Cannot have more than one '{0}' instance in the same AppDomain. + internal const string @MultiSingleton = "MultiSingleton"; + /// + internal const string @NavigateJournalKey = "NavigateJournalKey"; + /// + internal const string @NavigateJournalKeyDisplayString = "NavigateJournalKeyDisplayString"; + /// Navigate Journal + internal const string @NavigateJournalText = "NavigateJournalText"; + /// Ctrl+N + internal const string @NewKeyDisplayString = "NewKeyDisplayString"; + /// New + internal const string @NewText = "NewText"; + /// + internal const string @NextPageKey = "NextPageKey"; + /// + internal const string @NextPageKeyDisplayString = "NextPageKeyDisplayString"; + /// Next Page + internal const string @NextPageText = "NextPageText"; + /// Text formatting engine encountered a non-CLS exception. + internal const string @NonCLSException = "NonCLSException"; + /// Unsupported Uri syntax. Method expects a relative Uri or a pack://application:,,,/ form of absolute Uri. + internal const string @NonPackAppAbsoluteUriNotAllowed = "NonPackAppAbsoluteUriNotAllowed"; + /// Text content is not allowed on this element. Cannot add the text '{0}'. + internal const string @NonWhiteSpaceInAddText = "NonWhiteSpaceInAddText"; + /// Not a Command + internal const string @NotACommandText = "NotACommandText"; + /// The package URI is not allowed in the package store. + internal const string @NotAllowedPackageUri = "NotAllowedPackageUri"; + /// Only PreProcessInput and PostProcessInput events can access InputManager staging area. + internal const string @NotAllowedToAccessStagingArea = "NotAllowedToAccessStagingArea"; + /// The object is not being initialized. + internal const string @NotInInitialization = "NotInInitialization"; + /// '{0}' parameter cannot be null unless '{1}' is an absolute URI. + internal const string @NullBaseUriParam = "NullBaseUriParam"; + /// Hwnd of zero is not valid. + internal const string @NullHwnd = "NullHwnd"; + /// Offset must be non-negative. + internal const string @OffsetNegative = "OffsetNegative"; + /// OleRegisterDragDrop failed with return code '{0}' and window handle '{1}'. + internal const string @OleRegisterDragDropFailure = "OleRegisterDragDropFailure"; + /// OleRevokeDragDrop failed with return code '{0}' and window handle '{1}'. + internal const string @OleRevokeDragDropFailure = "OleRevokeDragDropFailure"; + /// OleInitialize failed for '{0}'. + internal const string @OleServicesContext_oleInitializeFailure = "OleServicesContext_oleInitializeFailure"; + /// Current thread must be set to single thread apartment (STA) mode before OLE calls can be made. + internal const string @OleServicesContext_ThreadMustBeSTA = "OleServicesContext_ThreadMustBeSTA"; + /// Keyboard processing can only process keyboard messages. + internal const string @OnlyAcceptsKeyMessages = "OnlyAcceptsKeyMessages"; + /// The object is already initialized and cannot be initialized again. + internal const string @OnlyOneInitialization = "OnlyOneInitialization"; + /// Ctrl+O + internal const string @OpenKeyDisplayString = "OpenKeyDisplayString"; + /// Open + internal const string @OpenText = "OpenText"; + /// Paragraph must be allowed to wrap in total-fit formatting. + internal const string @OptimalParagraphMustWrap = "OptimalParagraphMustWrap"; + /// A package with the same URI is already in the package store. + internal const string @PackageAlreadyExists = "PackageAlreadyExists"; + /// Cache policy is not valid. + internal const string @PackWebRequestCachePolicyIllegal = "PackWebRequestCachePolicyIllegal"; + /// Specified ContentPosition is not valid for this element. + internal const string @PaginatorMissingContentPosition = "PaginatorMissingContentPosition"; + /// Page number cannot be negative. + internal const string @PaginatorNegativePageNumber = "PaginatorNegativePageNumber"; + /// The parameter value cannot be greater than '{0}'. + internal const string @ParameterCannotBeGreaterThan = "ParameterCannotBeGreaterThan"; + /// The parameter value cannot be less than '{0}'. + internal const string @ParameterCannotBeLessThan = "ParameterCannotBeLessThan"; + /// Parameter must be greater than or equal to zero. + internal const string @ParameterCannotBeNegative = "ParameterCannotBeNegative"; + /// The parameter value must be between '{0}' and '{1}'. + internal const string @ParameterMustBeBetween = "ParameterMustBeBetween"; + /// The parameter value must be greater than zero. + internal const string @ParameterMustBeGreaterThanZero = "ParameterMustBeGreaterThanZero"; + /// The parameter value must be finite. + internal const string @ParameterValueCannotBeInfinity = "ParameterValueCannotBeInfinity"; + /// The parameter value must be a number. + internal const string @ParameterValueCannotBeNaN = "ParameterValueCannotBeNaN"; + /// '{0}' parameter value cannot be negative. + internal const string @ParameterValueCannotBeNegative = "ParameterValueCannotBeNegative"; + /// '{0}' parameter value must be greater than zero. + internal const string @ParameterValueMustBeGreaterThanZero = "ParameterValueMustBeGreaterThanZero"; + /// Token is not valid. + internal const string @Parsers_IllegalToken = "Parsers_IllegalToken"; + /// Token is not valid because it is more than 250 characters. + internal const string @Parsers_IllegalToken_250_Chars = "Parsers_IllegalToken_250_Chars"; + /// Incorrect form '{0}' found parsing '{1}' string. + internal const string @Parser_BadForm = "Parser_BadForm"; + /// Empty string not allowed. + internal const string @Parser_Empty = "Parser_Empty"; + /// Unexpected token '{0}' encountered at position '{1}'. + internal const string @Parser_UnexpectedToken = "Parser_UnexpectedToken"; + /// Ctrl+V;Shift+Insert + internal const string @PasteKeyDisplayString = "PasteKeyDisplayString"; + /// Paste + internal const string @PasteText = "PasteText"; + /// Internal error in newly produced path figures. + internal const string @PathGeometry_InternalReadBackError = "PathGeometry_InternalReadBackError"; + /// '{0}' file name is longer than the system-defined maximum length. + internal const string @PathTooLongExceptionWithFileName = "PathTooLongExceptionWithFileName"; + /// Cannot access a disposed pen service. + internal const string @Penservice_Disposed = "Penservice_Disposed"; + /// Unexpected size of packet from pen service. + internal const string @PenService_InvalidPacketData = "PenService_InvalidPacketData"; + /// The window is already registered for stylus input. + internal const string @PenService_WindowAlreadyRegistered = "PenService_WindowAlreadyRegistered"; + /// The window is not registered for stylus input. + internal const string @PenService_WindowNotRegistered = "PenService_WindowNotRegistered"; + /// + internal const string @PreviousPageKey = "PreviousPageKey"; + /// + internal const string @PreviousPageKeyDisplayString = "PreviousPageKeyDisplayString"; + /// Previous Page + internal const string @PreviousPageText = "PreviousPageText"; + /// Ctrl+P + internal const string @PrintKeyDisplayString = "PrintKeyDisplayString"; + /// Ctrl+F2 + internal const string @PrintPreviewKeyDisplayString = "PrintPreviewKeyDisplayString"; + /// Print Preview + internal const string @PrintPreviewText = "PrintPreviewText"; + /// Print + internal const string @PrintText = "PrintText"; + /// F4 + internal const string @PropertiesKeyDisplayString = "PropertiesKeyDisplayString"; + /// Properties + internal const string @PropertiesText = "PropertiesText"; + /// '{0}' property value must be greater than or equal to zero. + internal const string @PropertyCannotBeNegative = "PropertyCannotBeNegative"; + /// '{0}' property value must be greater than zero. + internal const string @PropertyMustBeGreaterThanZero = "PropertyMustBeGreaterThanZero"; + /// '{0}' property of the '{1}' class must be less than or equal to '{2}'. + internal const string @PropertyOfClassCannotBeGreaterThan = "PropertyOfClassCannotBeGreaterThan"; + /// '{0}' property of the '{1}' class cannot be null. + internal const string @PropertyOfClassCannotBeNull = "PropertyOfClassCannotBeNull"; + /// '{0}' property of the '{1}' class must be greater than zero. + internal const string @PropertyOfClassMustBeGreaterThanZero = "PropertyOfClassMustBeGreaterThanZero"; + /// '{0}' property value cannot be NaN. + internal const string @PropertyValueCannotBeNaN = "PropertyValueCannotBeNaN"; + /// Zero axis of rotation specified. + internal const string @Quaternion_ZeroAxisSpecified = "Quaternion_ZeroAxisSpecified"; + /// Text formatting engine cannot query text information due to error: '{0}'. + internal const string @QueryLineFailure = "QueryLineFailure"; + /// Count of bytes to read cannot be negative. + internal const string @ReadCountNegative = "ReadCountNegative"; + /// Operation not supported on a read-only InputGestureCollection. + internal const string @ReadOnlyInputGesturesCollection = "ReadOnlyInputGesturesCollection"; + /// Cannot call the method. + internal const string @Rect3D_CannotCallMethod = "Rect3D_CannotCallMethod"; + /// Cannot modify this property on the Empty Rect3D. + internal const string @Rect3D_CannotModifyEmptyRect = "Rect3D_CannotModifyEmptyRect"; + /// Rectangle cannot be empty. + internal const string @Rect_Empty = "Rect_Empty"; + /// Ctrl+Y + internal const string @RedoKeyDisplayString = "RedoKeyDisplayString"; + /// Redo + internal const string @RedoText = "RedoText"; + /// The visual tree has been changed during a '{0}' event. + internal const string @ReentrantVisualTreeChangeError = "ReentrantVisualTreeChangeError"; + /// WARNING. The visual tree has been changed during a '{0}' event. This is not supported in a production application. Be sure to correct this before shipping the application. + internal const string @ReentrantVisualTreeChangeWarning = "ReentrantVisualTreeChangeWarning"; + /// F5 + internal const string @RefreshKeyDisplayString = "RefreshKeyDisplayString"; + /// Refresh + internal const string @RefreshText = "RefreshText"; + /// Text formatting engine cannot release penalty resource due to error: '{0}'. + internal const string @RelievePenaltyResourceFailure = "RelievePenaltyResourceFailure"; + /// Ctrl+H + internal const string @ReplaceKeyDisplayString = "ReplaceKeyDisplayString"; + /// Replace + internal const string @ReplaceText = "ReplaceText"; + /// The operation is not allowed after the first request is made. + internal const string @RequestAlreadyStarted = "RequestAlreadyStarted"; + /// The calling thread must be STA, because many UI components require this. + internal const string @RequiresSTA = "RequiresSTA"; + /// Current CachePolicy is CacheOnly but the requested resource does not exist in the cache. + internal const string @ResourceNotFoundUnderCacheOnlyPolicy = "ResourceNotFoundUnderCacheOnlyPolicy"; + /// Every RoutedEventArgs must have a non-null RoutedEvent associated with it. + internal const string @RoutedEventArgsMustHaveRoutedEvent = "RoutedEventArgsMustHaveRoutedEvent"; + /// Cannot change the RoutedEvent property while the RoutedEvent is being routed. + internal const string @RoutedEventCannotChangeWhileRouting = "RoutedEventCannotChangeWhileRouting"; + /// Save As + internal const string @SaveAsText = "SaveAsText"; + /// Ctrl+S + internal const string @SaveKeyDisplayString = "SaveKeyDisplayString"; + /// Save + internal const string @SaveText = "SaveText"; + /// The Strokes have changed. + internal const string @SCDataChanged = "SCDataChanged"; + /// Path of erasing stroke cannot be null. + internal const string @SCErasePath = "SCErasePath"; + /// Erasing Shape cannot be null. + internal const string @SCEraseShape = "SCEraseShape"; + /// Cannot resolve current inner request URI schema. Bypass cache only for resolvable schema types such as http, ftp, or file. + internal const string @SchemaInvalidForTransport = "SchemaInvalidForTransport"; + /// The scope must be a UIElement or ContentElement. + internal const string @ScopeMustBeUIElementOrContent = "ScopeMustBeUIElementOrContent"; + /// + internal const string @ScrollByLineKey = "ScrollByLineKey"; + /// + internal const string @ScrollByLineKeyDisplayString = "ScrollByLineKeyDisplayString"; + /// Scroll By Line + internal const string @ScrollByLineText = "ScrollByLineText"; + /// PageDown + internal const string @ScrollPageDownKeyDisplayString = "ScrollPageDownKeyDisplayString"; + /// Scroll Page Down + internal const string @ScrollPageDownText = "ScrollPageDownText"; + /// + internal const string @ScrollPageLeftKey = "ScrollPageLeftKey"; + /// + internal const string @ScrollPageLeftKeyDisplayString = "ScrollPageLeftKeyDisplayString"; + /// Scroll Page Left + internal const string @ScrollPageLeftText = "ScrollPageLeftText"; + /// + internal const string @ScrollPageRightKey = "ScrollPageRightKey"; + /// + internal const string @ScrollPageRightKeyDisplayString = "ScrollPageRightKeyDisplayString"; + /// Scroll Page Right + internal const string @ScrollPageRightText = "ScrollPageRightText"; + /// PageUp + internal const string @ScrollPageUpKeyDisplayString = "ScrollPageUpKeyDisplayString"; + /// Scroll Page Up + internal const string @ScrollPageUpText = "ScrollPageUpText"; + /// F3 + internal const string @SearchKey = "SearchKey"; + /// F3 + internal const string @SearchKeyDisplayString = "SearchKeyDisplayString"; + /// Search + internal const string @SearchText = "SearchText"; + /// Cannot set SandboxExternalContent to true in partial trust. + internal const string @SecurityExceptionForSettingSandboxExternalToTrue = "SecurityExceptionForSettingSandboxExternalToTrue"; + /// Cannot set seek pointer to a negative position. + internal const string @SeekNegative = "SeekNegative"; + /// SeekOrigin value is not valid. + internal const string @SeekOriginInvalid = "SeekOriginInvalid"; + /// Ctrl+A + internal const string @SelectAllKeyDisplayString = "SelectAllKeyDisplayString"; + /// Select All + internal const string @SelectAllText = "SelectAllText"; + /// Shift+End + internal const string @SelectToEndKeyDisplayString = "SelectToEndKeyDisplayString"; + /// Select To End + internal const string @SelectToEndText = "SelectToEndText"; + /// Shift+Home + internal const string @SelectToHomeKeyDisplayString = "SelectToHomeKeyDisplayString"; + /// Select To Home + internal const string @SelectToHomeText = "SelectToHomeText"; + /// Shift+PageDown + internal const string @SelectToPageDownKeyDisplayString = "SelectToPageDownKeyDisplayString"; + /// Select To PageDown + internal const string @SelectToPageDownText = "SelectToPageDownText"; + /// Shift+PageUp + internal const string @SelectToPageUpKeyDisplayString = "SelectToPageUpKeyDisplayString"; + /// Select To PageUp + internal const string @SelectToPageUpText = "SelectToPageUpText"; + /// Text formatting engine cannot set breaking conditions due to error: '{0}'. + internal const string @SetBreakingFailure = "SetBreakingFailure"; + /// Text formatting engine cannot set document context due to error: '{0}'. + internal const string @SetDocFailure = "SetDocFailure"; + /// The target element cannot receive focus. + internal const string @SetFocusFailed = "SetFocusFailed"; + /// Stream does not support SetLength. + internal const string @SetLengthNotSupported = "SetLengthNotSupported"; + /// Text formatting engine cannot set tab stop due to error: '{0}'. + internal const string @SetTabsFailure = "SetTabsFailure"; + /// Sideways right to left text is not supported. + internal const string @SidewaysRTLTextIsNotSupported = "SidewaysRTLTextIsNotSupported"; + /// Cannot modify this property on the Empty Size3D. + internal const string @Size3D_CannotModifyEmptySize = "Size3D_CannotModifyEmptySize"; + /// Cannot set a negative dimension. + internal const string @Size3D_DimensionCannotBeNegative = "Size3D_DimensionCannotBeNegative"; + /// Must set Source in RoutedEventArgs before building event route or invoking handlers. + internal const string @SourceNotSet = "SourceNotSet"; + /// The CultureInfo object used for number substitution must be a specific culture, not a neutral culture or InvariantCulture. + internal const string @SpecificNumberCultureRequired = "SpecificNumberCultureRequired"; + /// Esc + internal const string @StopKeyDisplayString = "StopKeyDisplayString"; + /// Stop + internal const string @StopText = "StopText"; + /// BeginFigure must be called before this API. + internal const string @StreamGeometry_NeedBeginFigure = "StreamGeometry_NeedBeginFigure"; + /// Parameter cannot be a zero-length string. + internal const string @StringEmpty = "StringEmpty"; + /// Maximum number of strokes is two. + internal const string @StrokeCollectionCountTooBig = "StrokeCollectionCountTooBig"; + /// The specified StrokeCollection is read-only. + internal const string @StrokeCollectionIsReadOnly = "StrokeCollectionIsReadOnly"; + /// A duplicate stroke cannot be added to StrokeCollection. + internal const string @StrokeIsDuplicated = "StrokeIsDuplicated"; + /// The strokes being replaced must exist contiguously in the current StrokeCollection. + internal const string @StrokesNotContiguously = "StrokesNotContiguously"; + /// NotifyWhenProcessed can be called only during OnStylusDown, OnStylusMove, or OnStylusUp. + internal const string @Stylus_CanOnlyCallForDownMoveOrUp = "Stylus_CanOnlyCallForDownMoveOrUp"; + /// No current object to return. + internal const string @Stylus_EnumeratorFailure = "Stylus_EnumeratorFailure"; + /// '{0}' is not a valid index in the collection. + internal const string @Stylus_IndexOutOfRange = "Stylus_IndexOutOfRange"; + /// '{0}' must be greater than or equal to '{1}'. + internal const string @Stylus_InvalidMax = "Stylus_InvalidMax"; + /// Matrix is not invertible. + internal const string @Stylus_MatrixNotInvertable = "Stylus_MatrixNotInvertable"; + /// Stylus or Mouse must be in the down state when calling Reset. + internal const string @Stylus_MustBeDownToCallReset = "Stylus_MustBeDownToCallReset"; + /// Stylus input encountered an error. + internal const string @Stylus_PenContextFailure = "Stylus_PenContextFailure"; + /// '{0}' already exists in the collection. + internal const string @Stylus_PlugInIsDuplicated = "Stylus_PlugInIsDuplicated"; + /// '{0}' must be non-null. + internal const string @Stylus_PlugInIsNull = "Stylus_PlugInIsNull"; + /// '{0}' does not exist in the collection. + internal const string @Stylus_PlugInNotExist = "Stylus_PlugInNotExist"; + /// Count of points must be greater than zero. + internal const string @Stylus_StylusPointsCantBeEmpty = "Stylus_StylusPointsCantBeEmpty"; + /// Text breakpoint was previously disposed. + internal const string @TextBreakpointHasBeenDisposed = "TextBreakpointHasBeenDisposed"; + /// '{0}' does not have a valid InputManager. + internal const string @TextCompositionManager_NoInputManager = "TextCompositionManager_NoInputManager"; + /// '{0}' has already finished. + internal const string @TextCompositionManager_TextCompositionHasDone = "TextCompositionManager_TextCompositionHasDone"; + /// '{0}' has already started. + internal const string @TextCompositionManager_TextCompositionHasStarted = "TextCompositionManager_TextCompositionHasStarted"; + /// '{0}' has not yet started. + internal const string @TextCompositionManager_TextCompositionNotStarted = "TextCompositionManager_TextCompositionNotStarted"; + /// Result text cannot be null. + internal const string @TextComposition_NullResultText = "TextComposition_NullResultText"; + /// Cannot reenter Text formatting engine during optimal paragraph formatting. + internal const string @TextFormatterReentranceProhibited = "TextFormatterReentranceProhibited"; + /// Text line was previously disposed. + internal const string @TextLineHasBeenDisposed = "TextLineHasBeenDisposed"; + /// The return value of TextEmbeddedObject.Format contains an out-of-range value for the Width property. + internal const string @TextObjectMetrics_WidthOutOfRange = "TextObjectMetrics_WidthOutOfRange"; + /// Text penalty module was previously disposed. + internal const string @TextPenaltyModuleHasBeenDisposed = "TextPenaltyModuleHasBeenDisposed"; + /// '{0}' parameter value is not a valid child element of the text provider. + internal const string @TextProvider_InvalidChild = "TextProvider_InvalidChild"; + /// '{0}' parameter value is not a valid ITextRangeProvider. + internal const string @TextRangeProvider_InvalidRangeProvider = "TextRangeProvider_InvalidRangeProvider"; + /// The Properties member of this text run cannot be null. + internal const string @TextRunPropertiesCannotBeNull = "TextRunPropertiesCannotBeNull"; + /// The sum of AccelerationRatio and DecelerationRatio must be less than or equal to one. + internal const string @Timing_AccelAndDecelGreaterThanOne = "Timing_AccelAndDecelGreaterThanOne"; + /// CanSlip is supported only on timelines without AutoReverse, AccelerationRatio, or DecelerationRatio. + internal const string @Timing_CanSlipOnlyOnSimpleTimelines = "Timing_CanSlipOnlyOnSimpleTimelines"; + /// A child of a Timeline in "XAML" must also be a Timeline or a class that derives from Timeline. + internal const string @Timing_ChildMustBeTimeline = "Timing_ChildMustBeTimeline"; + /// The {0}.CreateClock method returned a pre-existing object, rather than a new object inheriting from TimelineClock. + internal const string @Timing_CreateClockMustReturnNewClock = "Timing_CreateClockMustReturnNewClock"; + /// The specified timeline belongs to a different thread than this timeline. + internal const string @Timing_DifferentThreads = "Timing_DifferentThreads"; + /// The enumeration is no longer valid because the collection it enumerates has changed. + internal const string @Timing_EnumeratorInvalidated = "Timing_EnumeratorInvalidated"; + /// The enumerator is out of range. + internal const string @Timing_EnumeratorOutOfRange = "Timing_EnumeratorOutOfRange"; + /// Property value must be between 0.0 and 1.0. + internal const string @Timing_InvalidArgAccelAndDecel = "Timing_InvalidArgAccelAndDecel"; + /// Property value must be finite and greater than or equal to zero. + internal const string @Timing_InvalidArgFiniteNonNegative = "Timing_InvalidArgFiniteNonNegative"; + /// Property value must be finite and greater than zero. + internal const string @Timing_InvalidArgFinitePositive = "Timing_InvalidArgFinitePositive"; + /// Property value must be greater than or equal to zero or indefinite. + internal const string @Timing_InvalidArgNonNegative = "Timing_InvalidArgNonNegative"; + /// Property value must be greater than zero or indefinite. + internal const string @Timing_InvalidArgPositive = "Timing_InvalidArgPositive"; + /// Timeline objects cannot have text objects as children. + internal const string @Timing_NoTextChildren = "Timing_NoTextChildren"; + /// Unable to return a TimeSpan property value for a Duration value of '{0}'. Check the HasTimeSpan property before requesting the TimeSpan property value from a Duration. + internal const string @Timing_NotTimeSpan = "Timing_NotTimeSpan"; + /// A timing operation has been not been queued in the appropriate order. + internal const string @Timing_OperationEnqueuedOutOfOrder = "Timing_OperationEnqueuedOutOfOrder"; + /// '{0}' is not a valid IterationCount value for a RepeatBehavior structure. An IterationCount value must represent a number that is greater than or equal to zero but not infinite. + internal const string @Timing_RepeatBehaviorInvalidIterationCount = "Timing_RepeatBehaviorInvalidIterationCount"; + /// '{0}' is not a valid RepeatDuration value for a RepeatBehavior structure. A RepeatDuration value must be a TimeSpan value greater than or equal to zero ticks. + internal const string @Timing_RepeatBehaviorInvalidRepeatDuration = "Timing_RepeatBehaviorInvalidRepeatDuration"; + /// '{0}' RepeatBehavior does not represent an iteration count and does not have an IterationCount value. + internal const string @Timing_RepeatBehaviorNotIterationCount = "Timing_RepeatBehaviorNotIterationCount"; + /// '{0}' RepeatBehavior does not represent a repeat duration and does not have a RepeatDuration value. + internal const string @Timing_RepeatBehaviorNotRepeatDuration = "Timing_RepeatBehaviorNotRepeatDuration"; + /// The ClockController.Seek method was called with arguments that describe a seek destination that seeks a child with Slip but no defined duration. It is unclear if we are seeking the child or seeking past the child's duration. + internal const string @Timing_SeekDestinationAmbiguousDueToSlip = "Timing_SeekDestinationAmbiguousDueToSlip"; + /// The ClockController.Seek method was called using TimeSeekOrigin.Duration as the seekOrigin parameter for a Clock that has a duration of Forever. Clocks that have duration of Forever must use TimeSeekOrigin.BeginTime. + internal const string @Timing_SeekDestinationIndefinite = "Timing_SeekDestinationIndefinite"; + /// The ClockController.Seek method was called with arguments that describe a seek destination with a negative value. The seek destination must be a time greater than or equal to zero. + internal const string @Timing_SeekDestinationNegative = "Timing_SeekDestinationNegative"; + /// Cannot call the ClockController.SkipToFill method for a Clock that has a Duration or RepeatDuration of Forever, because this Clock will never reach its fill period. + internal const string @Timing_SkipToFillDestinationIndefinite = "Timing_SkipToFillDestinationIndefinite"; + /// SlipBehavior.Slip is supported only on root ParallelTimelines that do not reverse, accelerate, decelerate, or have a RepeatBehavior specified as a Duration. + internal const string @Timing_SlipBehavior_SlipOnlyOnSimpleTimelines = "Timing_SlipBehavior_SlipOnlyOnSimpleTimelines"; + /// Clocks with CanSlip cannot have parents or ancestors with AutoReverse, AccelerationRatio, or DecelerationRatio. + internal const string @Timing_SlipBehavior_SyncOnlyWithSimpleParents = "Timing_SlipBehavior_SyncOnlyWithSimpleParents"; + /// Empty token encountered at position {0} while parsing '{1}'. + internal const string @TokenizerHelperEmptyToken = "TokenizerHelperEmptyToken"; + /// Extra data encountered at position {0} while parsing '{1}'. + internal const string @TokenizerHelperExtraDataEncountered = "TokenizerHelperExtraDataEncountered"; + /// Missing end quote encountered while parsing '{0}'. + internal const string @TokenizerHelperMissingEndQuote = "TokenizerHelperMissingEndQuote"; + /// Premature string termination encountered while parsing '{0}'. + internal const string @TokenizerHelperPrematureStringTermination = "TokenizerHelperPrematureStringTermination"; + /// Too many glyph runs in the scene to render. + internal const string @TooManyGlyphRuns = "TooManyGlyphRuns"; + /// RoutedEvent/EventPrivateKey limit exceeded. Routed events or EventPrivateKey for CLR events are typically static class members registered with field initializers or static constructors. In this case, routed events or EventPrivateKeys might be getting initi ... + internal const string @TooManyRoutedEvents = "TooManyRoutedEvents"; + /// Touch + internal const string @Touch_Category = "Touch_Category"; + /// The TouchDevice is already activated. + internal const string @Touch_DeviceAlreadyActivated = "Touch_DeviceAlreadyActivated"; + /// The TouchDevice is not activated. + internal const string @Touch_DeviceNotActivated = "Touch_DeviceNotActivated"; + /// Potential cycle in tree found while building the event route. + internal const string @TreeLoop = "TreeLoop"; + /// Cannot change property metadata after it has been associated with a property. + internal const string @TypeMetadataCannotChangeAfterUse = "TypeMetadataCannotChangeAfterUse"; + /// Cannot call Arrange on a UIElement with infinite size or NaN. Parent of type '{0}' invokes the UIElement. Arrange called on element of type '{1}'. + internal const string @UIElement_Layout_InfinityArrange = "UIElement_Layout_InfinityArrange"; + /// UIElement.Measure(availableSize) cannot be called with NaN size. + internal const string @UIElement_Layout_NaNMeasure = "UIElement_Layout_NaNMeasure"; + /// Layout measurement override of element '{0}' should not return NaN values as its DesiredSize. + internal const string @UIElement_Layout_NaNReturned = "UIElement_Layout_NaNReturned"; + /// Layout measurement override of element '{0}' should not return PositiveInfinity as its DesiredSize, even if Infinity is passed in as available size. + internal const string @UIElement_Layout_PositiveInfinityReturned = "UIElement_Layout_PositiveInfinityReturned"; + /// Access denied to the path '{0}'. + internal const string @UnauthorizedAccessExceptionWithFileName = "UnauthorizedAccessExceptionWithFileName"; + /// Ctrl+Z + internal const string @UndoKeyDisplayString = "UndoKeyDisplayString"; + /// Undo + internal const string @UndoText = "UndoText"; + /// Parameter is unexpected type '{0}'. Expected type is '{1}'. + internal const string @UnexpectedParameterType = "UnexpectedParameterType"; + /// Unexpected Stroke in PropertyDataChangedEventArgs.Owner. + internal const string @UnexpectedStroke = "UnexpectedStroke"; + /// Unknown path operation attempted. + internal const string @UnknownPathOperationType = "UnknownPathOperationType"; + /// Unrecognized Stroke in PropertyDataChangedEventArgs.Owner. + internal const string @UnknownStroke = "UnknownStroke"; + /// Unrecognized Stroke in Stroke.Invalidated event arguments. + internal const string @UnknownStroke1 = "UnknownStroke1"; + /// Unrecognized Stroke in StrokeCollectionChangedEventArgs.Removed. + internal const string @UnknownStroke3 = "UnknownStroke3"; + /// Failed to initialize GestureRecognizer. + internal const string @UnspecifiedGestureConstructionException = "UnspecifiedGestureConstructionException"; + /// Gesture recognition failed. + internal const string @UnspecifiedGestureException = "UnspecifiedGestureException"; + /// Failed to set enabled gestures. + internal const string @UnspecifiedSetEnabledGesturesException = "UnspecifiedSetEnabledGesturesException"; + /// Unsupported MouseAction '{0}'. + internal const string @Unsupported_MouseAction = "Unsupported_MouseAction"; + /// URI must be absolute. Relative URIs are not supported. + internal const string @UriMustBeAbsolute = "UriMustBeAbsolute"; + /// Font family Uri should have either file:// or pack://application: scheme. + internal const string @UriMustBeFileOrPack = "UriMustBeFileOrPack"; + /// URI must be absolute. + internal const string @UriNotAbsolute = "UriNotAbsolute"; + /// This factory supports only URIs with the '{0}' scheme. + internal const string @UriSchemeMismatch = "UriSchemeMismatch"; + /// UsesPerPixelOpacity is obsolete and should not be set when using UsesPerPixelTransparency + internal const string @UsesPerPixelOpacityIsObsolete = "UsesPerPixelOpacityIsObsolete"; + /// Value is not valid for the specified GUID. + internal const string @ValueNotValidForGuid = "ValueNotValidForGuid"; + /// MaterialGroup cannot be an interactive Material (IsVisualHostMaterial is true). + internal const string @Viewport2DVisual3D_MaterialGroupIsInteractiveMaterial = "Viewport2DVisual3D_MaterialGroupIsInteractiveMaterial"; + /// Viewport2DVisual3D supports only one interactive Material. + internal const string @Viewport2DVisual3D_MultipleInteractiveMaterials = "Viewport2DVisual3D_MultipleInteractiveMaterials"; + /// Specified Visual cannot be detached. + internal const string @VisualCannotBeDetached = "VisualCannotBeDetached"; + /// Specified index is already in use. Disconnect the Visual child at the specified index first. + internal const string @VisualCollection_EntryInUse = "VisualCollection_EntryInUse"; + /// Number of entries exceeds specified capacity of the VisualCollection. + internal const string @VisualCollection_NotEnoughCapacity = "VisualCollection_NotEnoughCapacity"; + /// This VisualCollection is read only and cannot be modified. + internal const string @VisualCollection_ReadOnly = "VisualCollection_ReadOnly"; + /// Specified Visual is already a child of another Visual or the root of a CompositionTarget. + internal const string @VisualCollection_VisualHasParent = "VisualCollection_VisualHasParent"; + /// Another target is already connected to this HostVisual. + internal const string @VisualTarget_AnotherTargetAlreadyConnected = "VisualTarget_AnotherTargetAlreadyConnected"; + /// Specified index is out of range or child at index is null. Do not call this method if VisualChildrenCount returns zero, indicating that the Visual has no children. + internal const string @Visual_ArgumentOutOfRange = "Visual_ArgumentOutOfRange"; + /// This Visual cannot transform the given point. + internal const string @Visual_CannotTransformPoint = "Visual_CannotTransformPoint"; + /// Must disconnect specified child from current parent Visual before attaching to new parent Visual. + internal const string @Visual_HasParent = "Visual_HasParent"; + /// The specified Visual and this Visual do not share a common ancestor, so there is no valid transformation between the two Visuals. + internal const string @Visual_NoCommonAncestor = "Visual_NoCommonAncestor"; + /// This Visual is not connected to a PresentationSource. + internal const string @Visual_NoPresentationSource = "Visual_NoPresentationSource"; + /// '{0}' is not a Visual3D. + internal const string @Visual_NotA3DVisual = "Visual_NotA3DVisual"; + /// The specified Visual is not a descendant of this Visual. + internal const string @Visual_NotADescendant = "Visual_NotADescendant"; + /// The specified Visual is not an ancestor of this Visual. + internal const string @Visual_NotAnAncestor = "Visual_NotAnAncestor"; + /// '{0}' is not a Visual or Visual3D. + internal const string @Visual_NotAVisual = "Visual_NotAVisual"; + /// Specified Visual is not a child of this Visual. + internal const string @Visual_NotChild = "Visual_NotChild"; + /// WebRequest timed out. Response did not arrive before the specified Timeout period elapsed. + internal const string @WebRequestTimeout = "WebRequestTimeout"; + /// Error closing the WebResponse. + internal const string @WebResponseCloseFailure = "WebResponseCloseFailure"; + /// Error processing WebResponse. + internal const string @WebResponseFailure = "WebResponseFailure"; + /// Requested PackagePart not found in target resource. + internal const string @WebResponsePartNotFound = "WebResponsePartNotFound"; + /// Object must be initialized before operation can be performed. + internal const string @WIC_NotInitialized = "WIC_NotInitialized"; + /// Stream does not support writing. + internal const string @WriteNotSupported = "WriteNotSupported"; + /// The required pattern for URI containing ";component" is "AssemblyName;Vxxxx;PublicKey;component", where Vxxxx is the assembly version and PublicKey is the 16-character string representing the assembly public key token. Vxxxx and PublicKey are optional. + internal const string @WrongFirstSegment = "WrongFirstSegment"; + /// There is no registered CultureInfo with the IetfLanguageTag '{0}'. + internal const string @XmlLangGetCultureFailure = "XmlLangGetCultureFailure"; + /// Cannot find non-neutral culture related to '{0}'. + internal const string @XmlLangGetSpecificCulture = "XmlLangGetSpecificCulture"; + /// '{0}' language tag must be empty or must conform to grammar defined in IETF RFC 3066. + internal const string @XmlLangMalformed = "XmlLangMalformed"; + /// + internal const string @ZoomKey = "ZoomKey"; + /// + internal const string @ZoomKeyDisplayString = "ZoomKeyDisplayString"; + /// Zoom + internal const string @ZoomText = "ZoomText"; + /// {0} failed to load from static constructor. + internal const string @PenImcDllVerificationFailed = "PenImcDllVerificationFailed"; + /// SxS COM registration of {0} failed. + internal const string @PenImcSxSRegistrationFailed = "PenImcSxSRegistrationFailed"; + + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/MS.Internal.WindowsBase.SRID.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/MS.Internal.WindowsBase.SRID.cs new file mode 100644 index 0000000..decb658 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/MS.Internal.WindowsBase.SRID.cs @@ -0,0 +1,1146 @@ +// +using System.Reflection; + +namespace FxResources.WindowsBase +{ + internal static class SR { } +} +namespace MS.Internal.WindowsBase +{ + internal static partial class SRID + { + private static global::System.Resources.ResourceManager s_resourceManager; + internal static global::System.Resources.ResourceManager ResourceManager => s_resourceManager ?? (s_resourceManager = new global::System.Resources.ResourceManager(typeof(FxResources.WindowsBase.SR))); + internal static global::System.Globalization.CultureInfo Culture { get; set; } + + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + internal static string GetResourceString(string resourceKey, string defaultValue = null) => ResourceManager.GetString(resourceKey, Culture); + /// en + internal const string WPF_UILanguage = nameof(WPF_UILanguage); + /// Cannot modify this property on the Empty Rect. + internal const string Rect_CannotModifyEmptyRect = nameof(Rect_CannotModifyEmptyRect); + /// Cannot call this method on the Empty Rect. + internal const string Rect_CannotCallMethod = nameof(Rect_CannotCallMethod); + /// Width and Height must be non-negative. + internal const string Size_WidthAndHeightCannotBeNegative = nameof(Size_WidthAndHeightCannotBeNegative); + /// Width must be non-negative. + internal const string Size_WidthCannotBeNegative = nameof(Size_WidthCannotBeNegative); + /// Height must be non-negative. + internal const string Size_HeightCannotBeNegative = nameof(Size_HeightCannotBeNegative); + /// Cannot modify this property on the Empty Size. + internal const string Size_CannotModifyEmptySize = nameof(Size_CannotModifyEmptySize); + /// Transform is not invertible. + internal const string Transform_NotInvertible = nameof(Transform_NotInvertible); + /// Expected object of type '{0}'. + internal const string General_Expected_Type = nameof(General_Expected_Type); + /// Value cannot be null. Object reference: '{0}'. + internal const string ReferenceIsNull = nameof(ReferenceIsNull); + /// The parameter value must be between '{0}' and '{1}'. + internal const string ParameterMustBeBetween = nameof(ParameterMustBeBetween); + /// Handler has not been registered with this event. + internal const string Freezable_UnregisteredHandler = nameof(Freezable_UnregisteredHandler); + /// Cannot use a DependencyObject that belongs to a different thread than its parent Freezable. + internal const string Freezable_AttemptToUseInnerValueWithDifferentThread = nameof(Freezable_AttemptToUseInnerValueWithDifferentThread); + /// This Freezable cannot be frozen. + internal const string Freezable_CantFreeze = nameof(Freezable_CantFreeze); + /// The provided DependencyObject is not a context for this Freezable. + internal const string Freezable_NotAContext = nameof(Freezable_NotAContext); + /// Cannot promote from '{0}' to '{1}' because the target map is too small. + internal const string FrugalList_TargetMapCannotHoldAllData = nameof(FrugalList_TargetMapCannotHoldAllData); + /// Cannot promote from Array. + internal const string FrugalList_CannotPromoteBeyondArray = nameof(FrugalList_CannotPromoteBeyondArray); + /// Cannot promote from '{0}' to '{1}' because the target map is too small. + internal const string FrugalMap_TargetMapCannotHoldAllData = nameof(FrugalMap_TargetMapCannotHoldAllData); + /// Cannot promote from Hashtable. + internal const string FrugalMap_CannotPromoteBeyondHashtable = nameof(FrugalMap_CannotPromoteBeyondHashtable); + /// Unrecognized Key '{0}'. + internal const string Unsupported_Key = nameof(Unsupported_Key); + /// Specified priority is not valid. + internal const string InvalidPriority = nameof(InvalidPriority); + /// The minimum priority must be less than or equal to the maximum priority. + internal const string InvalidPriorityRangeOrder = nameof(InvalidPriorityRangeOrder); + /// Cannot perform requested operation because the Dispatcher shut down. + internal const string DispatcherHasShutdown = nameof(DispatcherHasShutdown); + /// A thread cannot wait on operations already running on the same thread. + internal const string ThreadMayNotWaitOnOperationsAlreadyExecutingOnTheSameThread = nameof(ThreadMayNotWaitOnOperationsAlreadyExecutingOnTheSameThread); + /// The calling thread cannot access this object because a different thread owns it. + internal const string VerifyAccess = nameof(VerifyAccess); + /// Objects must be created by the same thread. + internal const string MismatchedDispatchers = nameof(MismatchedDispatchers); + /// Dispatcher processing has been suspended, but messages are still being processed. + internal const string DispatcherProcessingDisabledButStillPumping = nameof(DispatcherProcessingDisabledButStillPumping); + /// Cannot perform this operation while dispatcher processing is suspended. + internal const string DispatcherProcessingDisabled = nameof(DispatcherProcessingDisabled); + /// The DispatcherPriorityAwaiter was not configured with a valid Dispatcher. The only supported usage is from Dispatcher.Yield. + internal const string DispatcherPriorityAwaiterInvalid = nameof(DispatcherPriorityAwaiterInvalid); + /// The thread calling Dispatcher.Yield does not have a current Dispatcher. + internal const string DispatcherYieldNoAvailableDispatcher = nameof(DispatcherYieldNoAvailableDispatcher); + /// The Dispatcher is unable to request processing. This is often because the application has starved the Dispatcher's message pump. + internal const string DispatcherRequestProcessingFailed = nameof(DispatcherRequestProcessingFailed); + /// Exception Filter Code is not built and installed properly. + internal const string ExceptionFilterCodeNotPresent = nameof(ExceptionFilterCodeNotPresent); + /// Unrecognized ModifierKeys '{0}'. + internal const string Unsupported_Modifier = nameof(Unsupported_Modifier); + /// TimeSpan period must be greater than or equal to zero. + internal const string TimeSpanPeriodOutOfRange_TooSmall = nameof(TimeSpanPeriodOutOfRange_TooSmall); + /// TimeSpan period must be less than or equal to Int32.MaxValue. + internal const string TimeSpanPeriodOutOfRange_TooLarge = nameof(TimeSpanPeriodOutOfRange_TooLarge); + /// Cannot clear properties on object '{0}' because it is in a read-only state. + internal const string ClearOnReadOnlyObjectNotAllowed = nameof(ClearOnReadOnlyObjectNotAllowed); + /// Cannot automatically generate a valid default value for property '{0}'. Specify a default value explicitly when owner type '{1}' is registering this DependencyProperty. + internal const string DefaultValueAutoAssignFailed = nameof(DefaultValueAutoAssignFailed); + /// An Expression object is not a valid default value for a DependencyProperty. + internal const string DefaultValueMayNotBeExpression = nameof(DefaultValueMayNotBeExpression); + /// Default value cannot be 'Unset'. + internal const string DefaultValueMayNotBeUnset = nameof(DefaultValueMayNotBeUnset); + /// Default value for the '{0}' property cannot be bound to a specific thread. + internal const string DefaultValueMustBeFreeThreaded = nameof(DefaultValueMustBeFreeThreaded); + /// Default value type does not match type of property '{0}'. + internal const string DefaultValuePropertyTypeMismatch = nameof(DefaultValuePropertyTypeMismatch); + /// Default value for '{0}' property is not valid because ValidateValueCallback failed. + internal const string DefaultValueInvalid = nameof(DefaultValueInvalid); + /// '{0}' type does not have a matching DependencyObjectType. + internal const string DTypeNotSupportForSystemType = nameof(DTypeNotSupportForSystemType); + /// '{0}' is not a valid value for property '{1}'. + internal const string InvalidPropertyValue = nameof(InvalidPropertyValue); + /// Local value enumeration position is out of range. + internal const string LocalValueEnumerationOutOfBounds = nameof(LocalValueEnumerationOutOfBounds); + /// Local value enumeration position is before the start, need to call MoveNext first. + internal const string LocalValueEnumerationReset = nameof(LocalValueEnumerationReset); + /// Current local value enumeration is outdated because one or more local values have been set since its creation. + internal const string LocalValueEnumerationInvalidated = nameof(LocalValueEnumerationInvalidated); + /// Default value factory user must override PropertyMetadata.CreateDefaultValue. + internal const string MissingCreateDefaultValue = nameof(MissingCreateDefaultValue); + /// Metadata override and base metadata must be of the same type or derived type. + internal const string OverridingMetadataDoesNotMatchBaseMetadataType = nameof(OverridingMetadataDoesNotMatchBaseMetadataType); + /// '{0}' property was already registered by '{1}'. + internal const string PropertyAlreadyRegistered = nameof(PropertyAlreadyRegistered); + /// This method overrides metadata only on read-only properties. This property is not read-only. + internal const string PropertyNotReadOnly = nameof(PropertyNotReadOnly); + /// '{0}' property was registered as read-only and cannot be modified without an authorization key. + internal const string ReadOnlyChangeNotAllowed = nameof(ReadOnlyChangeNotAllowed); + /// Property key is not authorized to modify property '{0}'. + internal const string ReadOnlyKeyNotAuthorized = nameof(ReadOnlyKeyNotAuthorized); + /// '{0}' property was registered as read-only and its metadata cannot be overridden without an authorization key. + internal const string ReadOnlyOverrideNotAllowed = nameof(ReadOnlyOverrideNotAllowed); + /// Property key is not authorized to override metadata of property '{0}'. + internal const string ReadOnlyOverrideKeyNotAuthorized = nameof(ReadOnlyOverrideKeyNotAuthorized); + /// '{0}' is registered as read-only, so its value cannot be coerced by using the DesignerCoerceValueCallback. + internal const string ReadOnlyDesignerCoersionNotAllowed = nameof(ReadOnlyDesignerCoersionNotAllowed); + /// Cannot set a property on object '{0}' because it is in a read-only state. + internal const string SetOnReadOnlyObjectNotAllowed = nameof(SetOnReadOnlyObjectNotAllowed); + /// Shareable Expression cannot use ChangeSources method. + internal const string ShareableExpressionsCannotChangeSources = nameof(ShareableExpressionsCannotChangeSources); + /// Cannot set Expression. It is marked as 'NonShareable' and has already been used. + internal const string SharingNonSharableExpression = nameof(SharingNonSharableExpression); + /// ShouldSerializeProperty and ResetProperty methods must be public ('{0}'). + internal const string SpecialMethodMustBePublic = nameof(SpecialMethodMustBePublic); + /// Must create DependencySource on same Thread as the DependencyObject. + internal const string SourcesMustBeInSameThread = nameof(SourcesMustBeInSameThread); + /// Expression is not in use on DependencyObject. Cannot change DependencySource array. + internal const string SourceChangeExpressionMismatch = nameof(SourceChangeExpressionMismatch); + /// DependencyProperty limit has been exceeded upon registration of '{0}'. Dependency properties are normally static class members registered with static field initializers or static constructors. In this case, there may be dependency properties accidentally g ... + internal const string TooManyDependencyProperties = nameof(TooManyDependencyProperties); + /// Metadata is already associated with a type and property. A new one must be created. + internal const string TypeMetadataAlreadyInUse = nameof(TypeMetadataAlreadyInUse); + /// PropertyMetadata is already registered for type '{0}'. + internal const string TypeMetadataAlreadyRegistered = nameof(TypeMetadataAlreadyRegistered); + /// '{0}' type must derive from DependencyObject. + internal const string TypeMustBeDependencyObjectDerived = nameof(TypeMustBeDependencyObjectDerived); + /// Unrecognized Expression 'Mode' value. + internal const string UnknownExpressionMode = nameof(UnknownExpressionMode); + /// Buffer is too small to accommodate the specified parameters. + internal const string BufferTooSmall = nameof(BufferTooSmall); + /// Buffer offset cannot be negative. + internal const string BufferOffsetNegative = nameof(BufferOffsetNegative); + /// CompoundFile path must be non-empty. + internal const string CompoundFilePathNullEmpty = nameof(CompoundFilePathNullEmpty); + /// Cannot create new package on a read-only stream. + internal const string CanNotCreateContainerOnReadOnlyStream = nameof(CanNotCreateContainerOnReadOnlyStream); + /// Cannot create a read-only stream. + internal const string CanNotCreateAsReadOnly = nameof(CanNotCreateAsReadOnly); + /// Cannot create a stream in a read-only package. + internal const string CanNotCreateInReadOnly = nameof(CanNotCreateInReadOnly); + /// Cannot create StorageRoot on a nonreadable stream. + internal const string CanNotCreateStorageRootOnNonReadableStream = nameof(CanNotCreateStorageRootOnNonReadableStream); + /// Cannot delete element. + internal const string CanNotDelete = nameof(CanNotDelete); + /// Cannot delete element because access is denied. + internal const string CanNotDeleteAccessDenied = nameof(CanNotDeleteAccessDenied); + /// Cannot create data storage because access is denied. + internal const string CanNotCreateAccessDenied = nameof(CanNotCreateAccessDenied); + /// Cannot delete read-only packages. + internal const string CanNotDeleteInReadOnly = nameof(CanNotDeleteInReadOnly); + /// Cannot delete because the storage is not empty. Try a recursive delete with Delete(true). + internal const string CanNotDeleteNonEmptyStorage = nameof(CanNotDeleteNonEmptyStorage); + /// Cannot delete the root StorageInfo. + internal const string CanNotDeleteRoot = nameof(CanNotDeleteRoot); + /// Cannot perform this function on a storage that does not exist. + internal const string CanNotOnNonExistStorage = nameof(CanNotOnNonExistStorage); + /// Cannot open data storage. + internal const string CanNotOpenStorage = nameof(CanNotOpenStorage); + /// Cannot find specified package file. + internal const string ContainerNotFound = nameof(ContainerNotFound); + /// Cannot open specified package file. + internal const string ContainerCanNotOpen = nameof(ContainerCanNotOpen); + /// Create mode parameter must be either FileMode.Create or FileMode.Open. + internal const string CreateModeMustBeCreateOrOpen = nameof(CreateModeMustBeCreateOrOpen); + /// Compound File API failure. + internal const string CFAPIFailure = nameof(CFAPIFailure); + /// The given data space label name is already in use. + internal const string DataSpaceLabelInUse = nameof(DataSpaceLabelInUse); + /// Empty string is not a valid data space label. + internal const string DataSpaceLabelInvalidEmpty = nameof(DataSpaceLabelInvalidEmpty); + /// Specified data space label has not been defined. + internal const string DataSpaceLabelUndefined = nameof(DataSpaceLabelUndefined); + /// DataSpaceManager object was disposed. + internal const string DataSpaceManagerDisposed = nameof(DataSpaceManagerDisposed); + /// DataSpace map entry is not valid. + internal const string DataSpaceMapEntryInvalid = nameof(DataSpaceMapEntryInvalid); + /// FileAccess value is not valid. + internal const string FileAccessInvalid = nameof(FileAccessInvalid); + /// File already exists. + internal const string FileAlreadyExists = nameof(FileAlreadyExists); + /// FileMode value is not supported. + internal const string FileModeUnsupported = nameof(FileModeUnsupported); + /// FileMode value is not valid. + internal const string FileModeInvalid = nameof(FileModeInvalid); + /// FileShare value is not supported. + internal const string FileShareUnsupported = nameof(FileShareUnsupported); + /// FileShare value is not valid. + internal const string FileShareInvalid = nameof(FileShareInvalid); + /// Streams for exposure as ILockBytes must be seekable. + internal const string ILockBytesStreamMustSeek = nameof(ILockBytesStreamMustSeek); + /// '{1}' is not a valid value for '{0}'. + internal const string InvalidArgumentValue = nameof(InvalidArgumentValue); + /// Cannot locate information for stream that should exist. This is an internally inconsistent condition. + internal const string InvalidCondition01 = nameof(InvalidCondition01); + /// String format is not valid. + internal const string InvalidStringFormat = nameof(InvalidStringFormat); + /// Internal table type value is not valid. This is an internally inconsistent condition. + internal const string InvalidTableType = nameof(InvalidTableType); + /// MoveTo Destination storage does not exist. + internal const string MoveToDestNotExist = nameof(MoveToDestNotExist); + /// IStorage/IStream::MoveTo not supported. + internal const string MoveToNYI = nameof(MoveToNYI); + /// '{0}' name is already in use. + internal const string NameAlreadyInUse = nameof(NameAlreadyInUse); + /// '{0}' cannot contain the path delimiter: '{1}'. + internal const string NameCanNotHaveDelimiter = nameof(NameCanNotHaveDelimiter); + /// Failed call to '{0}'. + internal const string NamedAPIFailure = nameof(NamedAPIFailure); + /// Name table data is corrupt in data storage. + internal const string NameTableCorruptStg = nameof(NameTableCorruptStg); + /// Name table data is corrupt in memory. + internal const string NameTableCorruptMem = nameof(NameTableCorruptMem); + /// Name table cannot be read by this version of the program. + internal const string NameTableVersionMismatchRead = nameof(NameTableVersionMismatchRead); + /// Name table cannot be updated by this version of the program. + internal const string NameTableVersionMismatchWrite = nameof(NameTableVersionMismatchWrite); + /// This feature is not supported. + internal const string NYIDefault = nameof(NYIDefault); + /// Path string cannot include an empty element. + internal const string PathHasEmptyElement = nameof(PathHasEmptyElement); + /// Count of bytes to read cannot be negative. + internal const string ReadCountNegative = nameof(ReadCountNegative); + /// Cannot seek to given position. + internal const string SeekFailed = nameof(SeekFailed); + /// Cannot set seek pointer to a negative position. + internal const string SeekNegative = nameof(SeekNegative); + /// SeekOrigin value is not valid. + internal const string SeekOriginInvalid = nameof(SeekOriginInvalid); + /// This combination of flags is not supported. + internal const string StorageFlagsUnsupported = nameof(StorageFlagsUnsupported); + /// Storage already exists. + internal const string StorageAlreadyExist = nameof(StorageAlreadyExist); + /// Stream already exists. + internal const string StreamAlreadyExist = nameof(StreamAlreadyExist); + /// StorageInfo object was disposed. + internal const string StorageInfoDisposed = nameof(StorageInfoDisposed); + /// Storage does not exist. + internal const string StorageNotExist = nameof(StorageNotExist); + /// StorageRoot object was disposed. + internal const string StorageRootDisposed = nameof(StorageRootDisposed); + /// StreamInfo object was disposed. + internal const string StreamInfoDisposed = nameof(StreamInfoDisposed); + /// Stream length cannot be negative. + internal const string StreamLengthNegative = nameof(StreamLengthNegative); + /// Cannot perform this function on a stream that does not exist. + internal const string StreamNotExist = nameof(StreamNotExist); + /// Stream name cannot be '{0}'. + internal const string StreamNameNotValid = nameof(StreamNameNotValid); + /// Stream time stamp not implemented in OLE32 implementation of Compound Files. + internal const string StreamTimeStampNotImplemented = nameof(StreamTimeStampNotImplemented); + /// '{0}' cannot start with the reserved character range 0x01-0x1F. + internal const string StringCanNotBeReservedName = nameof(StringCanNotBeReservedName); + /// Requested time stamp is not available. + internal const string TimeStampNotAvailable = nameof(TimeStampNotAvailable); + /// Transform label name is already in use. + internal const string TransformLabelInUse = nameof(TransformLabelInUse); + /// Data space transform stack includes undefined transform labels. + internal const string TransformLabelUndefined = nameof(TransformLabelUndefined); + /// Transform object type is required to have a constructor which takes a TransformEnvironment object. + internal const string TransformObjectConstructorParam = nameof(TransformObjectConstructorParam); + /// Transform object type is required to implement IDataTransform interface. + internal const string TransformObjectImplementIDataTransform = nameof(TransformObjectImplementIDataTransform); + /// Stream transformation failed due to uninitialized data transform objects. + internal const string TransformObjectInitFailed = nameof(TransformObjectInitFailed); + /// Transform identifier type is not supported. + internal const string TransformTypeUnsupported = nameof(TransformTypeUnsupported); + /// Transform stack must have at least one transform. + internal const string TransformStackValid = nameof(TransformStackValid); + /// Cannot create package on stream. + internal const string UnableToCreateOnStream = nameof(UnableToCreateOnStream); + /// Cannot create data storage. + internal const string UnableToCreateStorage = nameof(UnableToCreateStorage); + /// Cannot create data stream. + internal const string UnableToCreateStream = nameof(UnableToCreateStream); + /// Cannot open data stream. + internal const string UnableToOpenStream = nameof(UnableToOpenStream); + /// Encountered unsupported type of storage element when building storage enumerator. + internal const string UnsupportedTypeEncounteredWhenBuildingStgEnum = nameof(UnsupportedTypeEncounteredWhenBuildingStgEnum); + /// Cannot write all data as specified. + internal const string WriteFailure = nameof(WriteFailure); + /// Write-only mode is not supported. + internal const string WriteOnlyUnsupported = nameof(WriteOnlyUnsupported); + /// Cannot write a negative number of bytes. + internal const string WriteSizeNegative = nameof(WriteSizeNegative); + /// Object metadata stream in the package is corrupt and the content is not valid. + internal const string CFM_CorruptMetadataStream = nameof(CFM_CorruptMetadataStream); + /// Object metadata stream in the package is corrupt and the root tag is not valid. + internal const string CFM_CorruptMetadataStream_Root = nameof(CFM_CorruptMetadataStream_Root); + /// Object metadata stream in the package is corrupt with duplicated key tags. + internal const string CFM_CorruptMetadataStream_DuplicateKey = nameof(CFM_CorruptMetadataStream_DuplicateKey); + /// Object used as metadata key must be an instance of the CompoundFileMetadataKey class. + internal const string CFM_ObjectMustBeCompoundFileMetadataKey = nameof(CFM_ObjectMustBeCompoundFileMetadataKey); + /// Cannot perform this operation when the package is in read-only mode. + internal const string CFM_ReadOnlyContainer = nameof(CFM_ReadOnlyContainer); + /// Failed to read a stream type table - the data appears to be a different format. + internal const string CFM_TypeTableFormat = nameof(CFM_TypeTableFormat); + /// Unicode character is not valid. + internal const string CFM_UnicodeCharInvalid = nameof(CFM_UnicodeCharInvalid); + /// Only strings can be used as value. + internal const string CFM_ValueMustBeString = nameof(CFM_ValueMustBeString); + /// XML character is not valid. + internal const string CFM_XMLCharInvalid = nameof(CFM_XMLCharInvalid); + /// Cannot compare different types. + internal const string CanNotCompareDiffTypes = nameof(CanNotCompareDiffTypes); + /// CompoundFileReference: Corrupted CompoundFileReference. + internal const string CFRCorrupt = nameof(CFRCorrupt); + /// CompoundFileReference: Corrupted CompoundFileReference - multiple stream components found. + internal const string CFRCorruptMultiStream = nameof(CFRCorruptMultiStream); + /// CompoundFileReference: Corrupted CompoundFileReference - storage component cannot follow stream component. + internal const string CFRCorruptStgFollowStm = nameof(CFRCorruptStgFollowStm); + /// Cannot have leading path delimiter. + internal const string DelimiterLeading = nameof(DelimiterLeading); + /// Cannot have trailing path delimiter. + internal const string DelimiterTrailing = nameof(DelimiterTrailing); + /// Offset must be greater than or equal to zero. + internal const string OffsetNegative = nameof(OffsetNegative); + /// Unrecognized reference component type. + internal const string UnknownReferenceComponentType = nameof(UnknownReferenceComponentType); + /// Cannot serialize unknown CompoundFileReference subclass. + internal const string UnknownReferenceSerialize = nameof(UnknownReferenceSerialize); + /// CompoundFileReference: malformed path encountered. + internal const string MalformedCompoundFilePath = nameof(MalformedCompoundFilePath); + /// Stream length cannot be negative. + internal const string CannotMakeStreamLengthNegative = nameof(CannotMakeStreamLengthNegative); + /// Stream operation failed because stream is corrupted. + internal const string CorruptStream = nameof(CorruptStream); + /// Stream does not support Length property. + internal const string LengthNotSupported = nameof(LengthNotSupported); + /// Buffer too small to hold results of Read. + internal const string ReadBufferTooSmall = nameof(ReadBufferTooSmall); + /// Stream does not support reading. + internal const string ReadNotSupported = nameof(ReadNotSupported); + /// Stream does not support Seek. + internal const string SeekNotSupported = nameof(SeekNotSupported); + /// Stream does not support SetLength. + internal const string SetLengthNotSupported = nameof(SetLengthNotSupported); + /// Stream does not support setting the Position property. + internal const string SetPositionNotSupported = nameof(SetPositionNotSupported); + /// Negative stream position not supported. + internal const string StreamPositionNegative = nameof(StreamPositionNegative); + /// Cannot change Transform parameters after the transform is initialized. + internal const string TransformParametersFixed = nameof(TransformParametersFixed); + /// Buffer of bytes to be written is too small. + internal const string WriteBufferTooSmall = nameof(WriteBufferTooSmall); + /// Count of bytes to write cannot be negative. + internal const string WriteCountNegative = nameof(WriteCountNegative); + /// Stream does not support writing. + internal const string WriteNotSupported = nameof(WriteNotSupported); + /// Compression requires ZLib library version {0}. + internal const string ZLibVersionError = nameof(ZLibVersionError); + /// Expected a VersionPair object. + internal const string ExpectedVersionPairObject = nameof(ExpectedVersionPairObject); + /// Major and minor version number components cannot be negative. + internal const string VersionNumberComponentNegative = nameof(VersionNumberComponentNegative); + /// Feature ID string cannot have zero length. + internal const string ZeroLengthFeatureID = nameof(ZeroLengthFeatureID); + /// Cannot find version stream. + internal const string VersionStreamMissing = nameof(VersionStreamMissing); + /// Cannot update version because of a version field size mismatch. + internal const string VersionUpdateFailure = nameof(VersionUpdateFailure); + /// Cannot remove signature from read-only file. + internal const string CannotRemoveSignatureFromReadOnlyFile = nameof(CannotRemoveSignatureFromReadOnlyFile); + /// Cannot sign read-only file. + internal const string CannotSignReadOnlyFile = nameof(CannotSignReadOnlyFile); + /// Cannot locate the selected digital certificate. + internal const string DigSigCannotLocateCertificate = nameof(DigSigCannotLocateCertificate); + /// Certificate error. Multiple certificates found with the same thumbprint. + internal const string DigSigDuplicateCertificate = nameof(DigSigDuplicateCertificate); + /// Digital Signature + internal const string CertSelectionDialogTitle = nameof(CertSelectionDialogTitle); + /// Select a certificate + internal const string CertSelectionDialogMessage = nameof(CertSelectionDialogMessage); + /// Duplicates not allowed - signature part already exists. + internal const string DuplicateSignature = nameof(DuplicateSignature); + /// Error parsing XML Signature. + internal const string XmlSignatureParseError = nameof(XmlSignatureParseError); + /// Required attribute '{0}' not found. + internal const string RequiredXmlAttributeMissing = nameof(RequiredXmlAttributeMissing); + /// Unexpected tag '{0}'. + internal const string UnexpectedXmlTag = nameof(UnexpectedXmlTag); + /// Required tag '{0}' not found. + internal const string RequiredTagNotFound = nameof(RequiredTagNotFound); + /// Required Package-specific Object tag is missing. + internal const string PackageSignatureObjectTagRequired = nameof(PackageSignatureObjectTagRequired); + /// Required Package-specific Reference tag is missing. + internal const string PackageSignatureReferenceTagRequired = nameof(PackageSignatureReferenceTagRequired); + /// Expected exactly one Package-specific Reference tag. + internal const string MoreThanOnePackageSpecificReference = nameof(MoreThanOnePackageSpecificReference); + /// Uri attribute in Reference tag must refer using fragment identifiers. + internal const string InvalidUriAttribute = nameof(InvalidUriAttribute); + /// Cannot countersign an unsigned package. + internal const string NoCounterSignUnsignedContainer = nameof(NoCounterSignUnsignedContainer); + /// Time format string is not valid. + internal const string BadSignatureTimeFormatString = nameof(BadSignatureTimeFormatString); + /// Signature structures are corrupted in this package. + internal const string PackageSignatureCorruption = nameof(PackageSignatureCorruption); + /// Unsupported hash algorithm specified. + internal const string UnsupportedHashAlgorithm = nameof(UnsupportedHashAlgorithm); + /// Relationship transform must be followed by an XML canonicalization transform. + internal const string RelationshipTransformNotFollowedByCanonicalizationTransform = nameof(RelationshipTransformNotFollowedByCanonicalizationTransform); + /// There must be at most one relationship transform specified for a given relationship part. + internal const string MultipleRelationshipTransformsFound = nameof(MultipleRelationshipTransformsFound); + /// Unsupported transform algorithm specified. + internal const string UnsupportedTransformAlgorithm = nameof(UnsupportedTransformAlgorithm); + /// Unsupported canonicalization method specified. + internal const string UnsupportedCanonicalizationMethod = nameof(UnsupportedCanonicalizationMethod); + /// Reusable hash algorithm must be specified. + internal const string HashAlgorithmMustBeReusable = nameof(HashAlgorithmMustBeReusable); + /// Malformed Part URI in Reference tag. + internal const string PartReferenceUriMalformed = nameof(PartReferenceUriMalformed); + /// Relationship was found to the signature origin but the part is missing. Package signature structures are corrupted. + internal const string SignatureOriginNotFound = nameof(SignatureOriginNotFound); + /// Multiple signature origin relationships found. + internal const string MultipleSignatureOrigins = nameof(MultipleSignatureOrigins); + /// Must specify an item to sign. + internal const string NothingToSign = nameof(NothingToSign); + /// Signature Identifier cannot be empty. + internal const string EmptySignatureId = nameof(EmptySignatureId); + /// Signature was deleted. + internal const string SignatureDeleted = nameof(SignatureDeleted); + /// Specified object ID conflicts with predefined Package Object ID. + internal const string SignaturePackageObjectTagMustBeUnique = nameof(SignaturePackageObjectTagMustBeUnique); + /// Specified reference object conflicts with predefined Package specific reference. + internal const string PackageSpecificReferenceTagMustBeUnique = nameof(PackageSpecificReferenceTagMustBeUnique); + /// Object identifiers must be unique within the same signature. + internal const string SignatureObjectIdMustBeUnique = nameof(SignatureObjectIdMustBeUnique); + /// Can only countersign parts with Digital Signature ContentType. + internal const string CanOnlyCounterSignSignatureParts = nameof(CanOnlyCounterSignSignatureParts); + /// Certificate part is not of the correct type. + internal const string CertificatePartContentTypeMismatch = nameof(CertificatePartContentTypeMismatch); + /// Signing certificate must be of type DSA or RSA. + internal const string CertificateKeyTypeNotSupported = nameof(CertificateKeyTypeNotSupported); + /// Specified part to sign does not exist. + internal const string PartToSignMissing = nameof(PartToSignMissing); + /// Duplicate object ID found. IDs must be unique within the signature XML. + internal const string DuplicateObjectId = nameof(DuplicateObjectId); + /// Caller-supplied parameter to callback function is not of expected type. + internal const string CallbackParameterInvalid = nameof(CallbackParameterInvalid); + /// Cannot change publish license after the rights management transform settings are fixed. + internal const string CannotChangePublishLicense = nameof(CannotChangePublishLicense); + /// Cannot change CryptoProvider after the rights management transform settings are fixed. + internal const string CannotChangeCryptoProvider = nameof(CannotChangeCryptoProvider); + /// Length prefix specifies {0} characters, which exceeds the maximum of {1} characters. + internal const string ExcessiveLengthPrefix = nameof(ExcessiveLengthPrefix); + /// OLE property ID {0} cannot be read (error {1}). + internal const string GetOlePropertyFailed = nameof(GetOlePropertyFailed); + /// Authentication type string (the part before the colon) is not valid in user ID '{0}'. + internal const string InvalidAuthenticationTypeString = nameof(InvalidAuthenticationTypeString); + /// '{0}' document property type is not valid. + internal const string InvalidDocumentPropertyType = nameof(InvalidDocumentPropertyType); + /// '{0}' document property variant type is not valid. + internal const string InvalidDocumentPropertyVariantType = nameof(InvalidDocumentPropertyVariantType); + /// User ID in use license stream is not of the form "authenticationType:userName". + internal const string InvalidTypePrefixedUserName = nameof(InvalidTypePrefixedUserName); + /// Feature name in the transform's primary stream is '{0}', but expected '{1}'. + internal const string InvalidTransformFeatureName = nameof(InvalidTransformFeatureName); + /// Document does not contain a package. + internal const string PackageNotFound = nameof(PackageNotFound); + /// File does not contain a stream to hold the publish license. + internal const string NoPublishLicenseStream = nameof(NoPublishLicenseStream); + /// File does not contain a storage to hold use licenses. + internal const string NoUseLicenseStorage = nameof(NoUseLicenseStorage); + /// File contains data in format version {0}, but the software can only read that data in format version {1} or lower. + internal const string ReaderVersionError = nameof(ReaderVersionError); + /// Document's publish license stream is corrupted. + internal const string PublishLicenseStreamCorrupt = nameof(PublishLicenseStreamCorrupt); + /// Document does not contain a publish license. + internal const string PublishLicenseNotFound = nameof(PublishLicenseNotFound); + /// Document does not contain any rights management-protected streams. + internal const string RightsManagementEncryptionTransformNotFound = nameof(RightsManagementEncryptionTransformNotFound); + /// Document contains multiple Rights Management Encryption Transforms. + internal const string MultipleRightsManagementEncryptionTransformFound = nameof(MultipleRightsManagementEncryptionTransformFound); + /// The stream on which the encrypted package is created must have read/write access. + internal const string StreamNeedsReadWriteAccess = nameof(StreamNeedsReadWriteAccess); + /// Cannot perform stream operation because CryptoProvider is not set to allow decryption. + internal const string CryptoProviderCanNotDecrypt = nameof(CryptoProviderCanNotDecrypt); + /// Only cryptographic providers based on a block cipher are supported. + internal const string CryptoProviderCanNotMergeBlocks = nameof(CryptoProviderCanNotMergeBlocks); + /// EncryptedPackageEnvelope object was disposed. + internal const string EncryptedPackageEnvelopeDisposed = nameof(EncryptedPackageEnvelopeDisposed); + /// CryptoProvider object was disposed. + internal const string CryptoProviderDisposed = nameof(CryptoProviderDisposed); + /// File contains data in format version {0}, but the software can only update that data in format version {1} or lower. + internal const string UpdaterVersionError = nameof(UpdaterVersionError); + /// The dictionary is read-only. + internal const string DictionaryIsReadOnly = nameof(DictionaryIsReadOnly); + /// The CryptoProvider cannot encrypt or decrypt. + internal const string CryptoProviderIsNotReady = nameof(CryptoProviderIsNotReady); + /// One of the document's use licenses is corrupted. + internal const string UseLicenseStreamCorrupt = nameof(UseLicenseStreamCorrupt); + /// Encrypted data stream is corrupted. + internal const string EncryptedDataStreamCorrupt = nameof(EncryptedDataStreamCorrupt); + /// Unrecognized document property: FMTID = '{0}', property ID = '{1}'. + internal const string UnknownDocumentProperty = nameof(UnknownDocumentProperty); + /// '{0}' document property in property set '{1}' is of incorrect variant type '{2}'. Expected type '{3}'. + internal const string WrongDocumentPropertyVariantType = nameof(WrongDocumentPropertyVariantType); + /// User is not activated. + internal const string UserIsNotActivated = nameof(UserIsNotActivated); + /// User does not have a client licensor certificate. + internal const string UserHasNoClientLicensorCert = nameof(UserHasNoClientLicensorCert); + /// Encryption right is not granted. + internal const string EncryptionRightIsNotGranted = nameof(EncryptionRightIsNotGranted); + /// Decryption right is not granted. + internal const string DecryptionRightIsNotGranted = nameof(DecryptionRightIsNotGranted); + /// CryptoProvider does not have privileges required for decryption of the PublishLicense. + internal const string NoPrivilegesForPublishLicenseDecryption = nameof(NoPrivilegesForPublishLicenseDecryption); + /// Signed Publish License is not valid. + internal const string InvalidPublishLicense = nameof(InvalidPublishLicense); + /// Variable-length header in publish license stream is {0} bytes, which exceeds the maximum length of {1} bytes. + internal const string PublishLicenseStreamHeaderTooLong = nameof(PublishLicenseStreamHeaderTooLong); + /// User must be either Windows or Passport authenticated. Other authentication types are not allowed in this context. + internal const string OnlyPassportOrWindowsAuthenticatedUsersAreAllowed = nameof(OnlyPassportOrWindowsAuthenticatedUsersAreAllowed); + /// Rights management operation failed. + internal const string RmExceptionGenericMessage = nameof(RmExceptionGenericMessage); + /// License is not valid. + internal const string RmExceptionInvalidLicense = nameof(RmExceptionInvalidLicense); + /// Information not found. + internal const string RmExceptionInfoNotInLicense = nameof(RmExceptionInfoNotInLicense); + /// License signature is not valid. + internal const string RmExceptionInvalidLicenseSignature = nameof(RmExceptionInvalidLicenseSignature); + /// Encryption not permitted. + internal const string RmExceptionEncryptionNotPermitted = nameof(RmExceptionEncryptionNotPermitted); + /// Right not granted. + internal const string RmExceptionRightNotGranted = nameof(RmExceptionRightNotGranted); + /// Version is not valid. + internal const string RmExceptionInvalidVersion = nameof(RmExceptionInvalidVersion); + /// Encoding type is not valid. + internal const string RmExceptionInvalidEncodingType = nameof(RmExceptionInvalidEncodingType); + /// Numerical value is not valid. + internal const string RmExceptionInvalidNumericalValue = nameof(RmExceptionInvalidNumericalValue); + /// Algorithm type is not valid. + internal const string RmExceptionInvalidAlgorithmType = nameof(RmExceptionInvalidAlgorithmType); + /// Environment not loaded. + internal const string RmExceptionEnvironmentNotLoaded = nameof(RmExceptionEnvironmentNotLoaded); + /// Cannot load environment. + internal const string RmExceptionEnvironmentCannotLoad = nameof(RmExceptionEnvironmentCannotLoad); + /// Cannot load more than one environment. + internal const string RmExceptionTooManyLoadedEnvironments = nameof(RmExceptionTooManyLoadedEnvironments); + /// Incompatible objects. + internal const string RmExceptionIncompatibleObjects = nameof(RmExceptionIncompatibleObjects); + /// Library fail. + internal const string RmExceptionLibraryFail = nameof(RmExceptionLibraryFail); + /// Enabling principal failure. + internal const string RmExceptionEnablingPrincipalFailure = nameof(RmExceptionEnablingPrincipalFailure); + /// Information not found. + internal const string RmExceptionInfoNotPresent = nameof(RmExceptionInfoNotPresent); + /// Get information query is not valid. + internal const string RmExceptionBadGetInfoQuery = nameof(RmExceptionBadGetInfoQuery); + /// Key type not supported. + internal const string RmExceptionKeyTypeUnsupported = nameof(RmExceptionKeyTypeUnsupported); + /// Crypto operation not supported. + internal const string RmExceptionCryptoOperationUnsupported = nameof(RmExceptionCryptoOperationUnsupported); + /// Clock rollback detected. + internal const string RmExceptionClockRollbackDetected = nameof(RmExceptionClockRollbackDetected); + /// Query reports no results. + internal const string RmExceptionQueryReportsNoResults = nameof(RmExceptionQueryReportsNoResults); + /// Unexpected exception. + internal const string RmExceptionUnexpectedException = nameof(RmExceptionUnexpectedException); + /// Binding validity time violated. + internal const string RmExceptionBindValidityTimeViolated = nameof(RmExceptionBindValidityTimeViolated); + /// Broken certificate chain. + internal const string RmExceptionBrokenCertChain = nameof(RmExceptionBrokenCertChain); + /// Binding policy violation. + internal const string RmExceptionBindPolicyViolation = nameof(RmExceptionBindPolicyViolation); + /// Manifest policy violation. + internal const string RmExceptionManifestPolicyViolation = nameof(RmExceptionManifestPolicyViolation); + /// License has been revoked. + internal const string RmExceptionBindRevokedLicense = nameof(RmExceptionBindRevokedLicense); + /// Issuer has been revoked. + internal const string RmExceptionBindRevokedIssuer = nameof(RmExceptionBindRevokedIssuer); + /// Principal has been revoked. + internal const string RmExceptionBindRevokedPrincipal = nameof(RmExceptionBindRevokedPrincipal); + /// Resource has been revoked. + internal const string RmExceptionBindRevokedResource = nameof(RmExceptionBindRevokedResource); + /// Module has been revoked. + internal const string RmExceptionBindRevokedModule = nameof(RmExceptionBindRevokedModule); + /// Binding content not in the End Use License. + internal const string RmExceptionBindContentNotInEndUseLicense = nameof(RmExceptionBindContentNotInEndUseLicense); + /// Binding access principal is not enabling. + internal const string RmExceptionBindAccessPrincipalNotEnabling = nameof(RmExceptionBindAccessPrincipalNotEnabling); + /// Binding access unsatisfied. + internal const string RmExceptionBindAccessUnsatisfied = nameof(RmExceptionBindAccessUnsatisfied); + /// Principal provided for binding is missing. + internal const string RmExceptionBindIndicatedPrincipalMissing = nameof(RmExceptionBindIndicatedPrincipalMissing); + /// Machine is not found in group identity certificate. + internal const string RmExceptionBindMachineNotFoundInGroupIdentity = nameof(RmExceptionBindMachineNotFoundInGroupIdentity); + /// Unsupported library plug-in. + internal const string RmExceptionLibraryUnsupportedPlugIn = nameof(RmExceptionLibraryUnsupportedPlugIn); + /// Binding revocation list is stale. + internal const string RmExceptionBindRevocationListStale = nameof(RmExceptionBindRevocationListStale); + /// Binding missing application revocation list. + internal const string RmExceptionBindNoApplicableRevocationList = nameof(RmExceptionBindNoApplicableRevocationList); + /// Handle is not valid. + internal const string RmExceptionInvalidHandle = nameof(RmExceptionInvalidHandle); + /// Binding time interval is violated. + internal const string RmExceptionBindIntervalTimeViolated = nameof(RmExceptionBindIntervalTimeViolated); + /// Binding cannot find a satisfied rights group. + internal const string RmExceptionBindNoSatisfiedRightsGroup = nameof(RmExceptionBindNoSatisfiedRightsGroup); + /// Cannot find content specified for binding. + internal const string RmExceptionBindSpecifiedWorkMissing = nameof(RmExceptionBindSpecifiedWorkMissing); + /// No more data. + internal const string RmExceptionNoMoreData = nameof(RmExceptionNoMoreData); + /// License acquisition failed. + internal const string RmExceptionLicenseAcquisitionFailed = nameof(RmExceptionLicenseAcquisitionFailed); + /// ID mismatch. + internal const string RmExceptionIdMismatch = nameof(RmExceptionIdMismatch); + /// Cannot have more than one certificate. + internal const string RmExceptionTooManyCertificates = nameof(RmExceptionTooManyCertificates); + /// Distribution Point URL was not set. + internal const string RmExceptionNoDistributionPointUrlFound = nameof(RmExceptionNoDistributionPointUrlFound); + /// Rights management server transaction already in progress. + internal const string RmExceptionAlreadyInProgress = nameof(RmExceptionAlreadyInProgress); + /// Group identity not set. + internal const string RmExceptionGroupIdentityNotSet = nameof(RmExceptionGroupIdentityNotSet); + /// Record not found. + internal const string RmExceptionRecordNotFound = nameof(RmExceptionRecordNotFound); + /// Connection failed. + internal const string RmExceptionNoConnect = nameof(RmExceptionNoConnect); + /// License not found. + internal const string RmExceptionNoLicense = nameof(RmExceptionNoLicense); + /// Machine must be activated. + internal const string RmExceptionNeedsMachineActivation = nameof(RmExceptionNeedsMachineActivation); + /// User identity must be activated. + internal const string RmExceptionNeedsGroupIdentityActivation = nameof(RmExceptionNeedsGroupIdentityActivation); + /// Activation failed. + internal const string RmExceptionActivationFailed = nameof(RmExceptionActivationFailed); + /// Command interrupted. + internal const string RmExceptionAborted = nameof(RmExceptionAborted); + /// Transaction quota exceeded. + internal const string RmExceptionOutOfQuota = nameof(RmExceptionOutOfQuota); + /// Authentication failed. + internal const string RmExceptionAuthenticationFailed = nameof(RmExceptionAuthenticationFailed); + /// Server side error. + internal const string RmExceptionServerError = nameof(RmExceptionServerError); + /// Installation failed. + internal const string RmExceptionInstallationFailed = nameof(RmExceptionInstallationFailed); + /// Hardware ID corrupted. + internal const string RmExceptionHidCorrupted = nameof(RmExceptionHidCorrupted); + /// Server response is not valid. + internal const string RmExceptionInvalidServerResponse = nameof(RmExceptionInvalidServerResponse); + /// Service not found. + internal const string RmExceptionServiceNotFound = nameof(RmExceptionServiceNotFound); + /// Use default. + internal const string RmExceptionUseDefault = nameof(RmExceptionUseDefault); + /// Server not found. + internal const string RmExceptionServerNotFound = nameof(RmExceptionServerNotFound); + /// E-mail address is not valid. + internal const string RmExceptionInvalidEmail = nameof(RmExceptionInvalidEmail); + /// License validity time violation. + internal const string RmExceptionValidityTimeViolation = nameof(RmExceptionValidityTimeViolation); + /// Outdated module. + internal const string RmExceptionOutdatedModule = nameof(RmExceptionOutdatedModule); + /// Service moved. + internal const string RmExceptionServiceMoved = nameof(RmExceptionServiceMoved); + /// Service gone. + internal const string RmExceptionServiceGone = nameof(RmExceptionServiceGone); + /// Ad entry not found. + internal const string RmExceptionAdEntryNotFound = nameof(RmExceptionAdEntryNotFound); + /// Not a certificate chain. + internal const string RmExceptionNotAChain = nameof(RmExceptionNotAChain); + /// Rights management server denied request. + internal const string RmExceptionRequestDenied = nameof(RmExceptionRequestDenied); + /// Not set. + internal const string RmExceptionNotSet = nameof(RmExceptionNotSet); + /// Metadata not set. + internal const string RmExceptionMetadataNotSet = nameof(RmExceptionMetadataNotSet); + /// Revocation information not set. + internal const string RmExceptionRevocationInfoNotSet = nameof(RmExceptionRevocationInfoNotSet); + /// Time information is not valid. + internal const string RmExceptionInvalidTimeInfo = nameof(RmExceptionInvalidTimeInfo); + /// Right not set. + internal const string RmExceptionRightNotSet = nameof(RmExceptionRightNotSet); + /// License binding to Windows Identity failed (NTLM bind failure). + internal const string RmExceptionLicenseBindingToWindowsIdentityFailed = nameof(RmExceptionLicenseBindingToWindowsIdentityFailed); + /// Issuance license template is not valid because of incorrectly formatted string. + internal const string RmExceptionInvalidIssuanceLicenseTemplate = nameof(RmExceptionInvalidIssuanceLicenseTemplate); + /// Key size length is not valid. + internal const string RmExceptionInvalidKeyLength = nameof(RmExceptionInvalidKeyLength); + /// Expired official Publish License template. + internal const string RmExceptionExpiredOfficialIssuanceLicenseTemplate = nameof(RmExceptionExpiredOfficialIssuanceLicenseTemplate); + /// Client Licensor Certificate is not valid. + internal const string RmExceptionInvalidClientLicensorCertificate = nameof(RmExceptionInvalidClientLicensorCertificate); + /// Hardware ID is not valid. + internal const string RmExceptionHidInvalid = nameof(RmExceptionHidInvalid); + /// E-mail not verified. + internal const string RmExceptionEmailNotVerified = nameof(RmExceptionEmailNotVerified); + /// Debugger detected. + internal const string RmExceptionDebuggerDetected = nameof(RmExceptionDebuggerDetected); + /// Lockbox type is not valid. + internal const string RmExceptionInvalidLockboxType = nameof(RmExceptionInvalidLockboxType); + /// Lockbox path is not valid. + internal const string RmExceptionInvalidLockboxPath = nameof(RmExceptionInvalidLockboxPath); + /// Registry path is not valid. + internal const string RmExceptionInvalidRegistryPath = nameof(RmExceptionInvalidRegistryPath); + /// No AES Crypto provider found. + internal const string RmExceptionNoAesCryptoProvider = nameof(RmExceptionNoAesCryptoProvider); + /// Global option is already set. + internal const string RmExceptionGlobalOptionAlreadySet = nameof(RmExceptionGlobalOptionAlreadySet); + /// Owner's license not found. + internal const string RmExceptionOwnerLicenseNotFound = nameof(RmExceptionOwnerLicenseNotFound); + /// Archive file cannot be size 0. + internal const string ZipZeroSizeFileIsNotValidArchive = nameof(ZipZeroSizeFileIsNotValidArchive); + /// Cannot perform a write operation in read-only mode. + internal const string CanNotWriteInReadOnlyMode = nameof(CanNotWriteInReadOnlyMode); + /// Cannot perform a read operation in write-only mode. + internal const string CanNotReadInWriteOnlyMode = nameof(CanNotReadInWriteOnlyMode); + /// Cannot perform a read/write operation in write-only or read-only modes. + internal const string CanNotReadWriteInReadOnlyWriteOnlyMode = nameof(CanNotReadWriteInReadOnlyWriteOnlyMode); + /// Cannot create file because the specified file name is already in use. + internal const string AttemptedToCreateDuplicateFileName = nameof(AttemptedToCreateDuplicateFileName); + /// Cannot find specified file. + internal const string FileDoesNotExists = nameof(FileDoesNotExists); + /// Truncate and Append FileModes are not supported. + internal const string TruncateAppendModesNotSupported = nameof(TruncateAppendModesNotSupported); + /// Only FileShare.Read and FileShare.None are supported. + internal const string OnlyFileShareReadAndFileShareNoneSupported = nameof(OnlyFileShareReadAndFileShareNoneSupported); + /// Cannot read data from stream that does not support reading. + internal const string CanNotReadDataFromStreamWhichDoesNotSupportReading = nameof(CanNotReadDataFromStreamWhichDoesNotSupportReading); + /// Cannot write data to stream that does not support writing. + internal const string CanNotWriteDataToStreamWhichDoesNotSupportWriting = nameof(CanNotWriteDataToStreamWhichDoesNotSupportWriting); + /// Cannot operate on stream that does not support seeking. + internal const string CanNotOperateOnStreamWhichDoesNotSupportSeeking = nameof(CanNotOperateOnStreamWhichDoesNotSupportSeeking); + /// Cannot get stream with FileMode.Create, FileMode.CreateNew, FileMode.Truncate, FileMode.Append when access is FileAccess.Read. + internal const string UnsupportedCombinationOfModeAccessShareStreaming = nameof(UnsupportedCombinationOfModeAccessShareStreaming); + /// File contains corrupted data. + internal const string CorruptedData = nameof(CorruptedData); + /// Multidisk ZIP format is not supported. + internal const string NotSupportedMultiDisk = nameof(NotSupportedMultiDisk); + /// ZIP archive was closed and disposed. + internal const string ZipArchiveDisposed = nameof(ZipArchiveDisposed); + /// ZIP file was closed, disposed, or deleted. + internal const string ZipFileItemDisposed = nameof(ZipFileItemDisposed); + /// ZIP archive contains unsupported data structures. + internal const string NotSupportedVersionNeededToExtract = nameof(NotSupportedVersionNeededToExtract); + /// ZIP archive contains data structures too large to fit in memory. + internal const string Zip64StructuresTooLarge = nameof(Zip64StructuresTooLarge); + /// ZIP archive contains unsupported encrypted data. + internal const string ZipNotSupportedEncryptedArchive = nameof(ZipNotSupportedEncryptedArchive); + /// ZIP archive contains unsupported signature data. + internal const string ZipNotSupportedSignedArchive = nameof(ZipNotSupportedSignedArchive); + /// ZIP archive contains data compressed using an unsupported algorithm. + internal const string ZipNotSupportedCompressionMethod = nameof(ZipNotSupportedCompressionMethod); + /// Compressed part has inconsistent data length. + internal const string CompressLengthMismatch = nameof(CompressLengthMismatch); + /// CreateNew is not a valid FileMode for a nonempty stream. + internal const string CreateNewOnNonEmptyStream = nameof(CreateNewOnNonEmptyStream); + /// Specified part does not exist in the package. + internal const string PartDoesNotExist = nameof(PartDoesNotExist); + /// Cannot add part for the specified URI because it is already in the package. + internal const string PartAlreadyExists = nameof(PartAlreadyExists); + /// Cannot add part to the package. Part names cannot be derived from another part name by appending segments to it. + internal const string PartNamePrefixExists = nameof(PartNamePrefixExists); + /// Cannot open package because FileMode or FileAccess value is not valid for the stream. + internal const string IncompatibleModeOrAccess = nameof(IncompatibleModeOrAccess); + /// Cannot be an absolute URI. + internal const string URIShouldNotBeAbsolute = nameof(URIShouldNotBeAbsolute); + /// Must have absolute URI. + internal const string UriShouldBeAbsolute = nameof(UriShouldBeAbsolute); + /// FileMode/FileAccess for Part.GetStream is not compatible with FileMode/FileAccess used to open the Package. + internal const string ContainerAndPartModeIncompatible = nameof(ContainerAndPartModeIncompatible); + /// Cannot get stream with FileMode.Create, FileMode.CreateNew, FileMode.Truncate, FileMode.Append when access is FileAccess.Read. + internal const string UnsupportedCombinationOfModeAccess = nameof(UnsupportedCombinationOfModeAccess); + /// Returned stream for the part is null. + internal const string NullStreamReturned = nameof(NullStreamReturned); + /// Package object was closed and disposed, so cannot carry out operations on this object or any stream opened on a part of this package. + internal const string ObjectDisposed = nameof(ObjectDisposed); + /// Cannot write to read-only stream. + internal const string ReadOnlyStream = nameof(ReadOnlyStream); + /// Cannot read from write-only stream. + internal const string WriteOnlyStream = nameof(WriteOnlyStream); + /// Cannot access part because parent package was closed. + internal const string ParentContainerClosed = nameof(ParentContainerClosed); + /// Part was deleted. + internal const string PackagePartDeleted = nameof(PackagePartDeleted); + /// PackageRelationship cannot target another PackageRelationship. + internal const string RelationshipToRelationshipIllegal = nameof(RelationshipToRelationshipIllegal); + /// PackageRelationship parts cannot have relationships to other parts. + internal const string RelationshipPartsCannotHaveRelationships = nameof(RelationshipPartsCannotHaveRelationships); + /// Incorrect content type for PackageRelationship part. + internal const string RelationshipPartIncorrectContentType = nameof(RelationshipPartIncorrectContentType); + /// PackageRelationship with specified ID does not exist at the Package level. + internal const string PackageRelationshipDoesNotExist = nameof(PackageRelationshipDoesNotExist); + /// PackageRelationship with specified ID does not exist for the source part. + internal const string PackagePartRelationshipDoesNotExist = nameof(PackagePartRelationshipDoesNotExist); + /// PackageRelationship target must be relative URI if TargetMode is Internal. + internal const string RelationshipTargetMustBeRelative = nameof(RelationshipTargetMustBeRelative); + /// Relationship tag requires attribute '{0}'. + internal const string RequiredRelationshipAttributeMissing = nameof(RequiredRelationshipAttributeMissing); + /// Relationship tag contains incorrect attribute. + internal const string RelationshipTagDoesntMatchSchema = nameof(RelationshipTagDoesntMatchSchema); + /// Relationships tag has extra attributes. + internal const string RelationshipsTagHasExtraAttributes = nameof(RelationshipsTagHasExtraAttributes); + /// Unrecognized tag found in Relationships XML. + internal const string UnknownTagEncountered = nameof(UnknownTagEncountered); + /// Relationships tag expected at root level. + internal const string ExpectedRelationshipsElementTag = nameof(ExpectedRelationshipsElementTag); + /// Relationships XML elements cannot specify attribute '{0}'. + internal const string InvalidXmlBaseAttributePresent = nameof(InvalidXmlBaseAttributePresent); + /// '{0}' ID conflicts with the ID of an existing relationship for the specified source. + internal const string NotAUniqueRelationshipId = nameof(NotAUniqueRelationshipId); + /// '{0}' ID is not a valid XSD ID. + internal const string NotAValidXmlIdString = nameof(NotAValidXmlIdString); + /// '{0}' attribute value is not valid. + internal const string InvalidValueForTheAttribute = nameof(InvalidValueForTheAttribute); + /// Relationship Type cannot contain only spaces or be empty. + internal const string InvalidRelationshipType = nameof(InvalidRelationshipType); + /// Part URI must start with a forward slash. + internal const string PartUriShouldStartWithForwardSlash = nameof(PartUriShouldStartWithForwardSlash); + /// Part URI cannot end with a forward slash. + internal const string PartUriShouldNotEndWithForwardSlash = nameof(PartUriShouldNotEndWithForwardSlash); + /// URI must contain pack:// scheme. + internal const string UriShouldBePackScheme = nameof(UriShouldBePackScheme); + /// Part URI is empty. + internal const string PartUriIsEmpty = nameof(PartUriIsEmpty); + /// Part URI is not valid per rules defined in the Open Packaging Conventions specification. + internal const string InvalidPartUri = nameof(InvalidPartUri); + /// PackageRelationship part URI is not expected. + internal const string RelationshipPartUriNotExpected = nameof(RelationshipPartUriNotExpected); + /// PackageRelationship part URI is expected. + internal const string RelationshipPartUriExpected = nameof(RelationshipPartUriExpected); + /// PackageRelationship part URI syntax is not valid. + internal const string NotAValidRelationshipPartUri = nameof(NotAValidRelationshipPartUri); + /// The 'fragment' parameter must start with a number sign. + internal const string FragmentMustStartWithHash = nameof(FragmentMustStartWithHash); + /// Part URI cannot contain a Fragment component. + internal const string PartUriCannotHaveAFragment = nameof(PartUriCannotHaveAFragment); + /// Part URI cannot start with two forward slashes. + internal const string PartUriShouldNotStartWithTwoForwardSlashes = nameof(PartUriShouldNotStartWithTwoForwardSlashes); + /// Package URI obtained from the pack URI cannot contain a Fragment. + internal const string InnerPackageUriHasFragment = nameof(InnerPackageUriHasFragment); + /// Cannot access Stream object because it was closed or disposed. + internal const string StreamObjectDisposed = nameof(StreamObjectDisposed); + /// GetContentTypeCore method cannot return null for the content type stream. + internal const string NullContentTypeProvided = nameof(NullContentTypeProvided); + /// PackagePart subclass must implement GetContentTypeCore method if passing a null value for the content type when PackagePart object is constructed. + internal const string GetContentTypeCoreNotImplemented = nameof(GetContentTypeCoreNotImplemented); + /// '{0}' tag requires attribute '{1}'. + internal const string RequiredAttributeMissing = nameof(RequiredAttributeMissing); + /// '{0}' tag requires a nonempty '{1}' attribute. + internal const string RequiredAttributeEmpty = nameof(RequiredAttributeEmpty); + /// Types tag has attributes not valid per the schema. + internal const string TypesTagHasExtraAttributes = nameof(TypesTagHasExtraAttributes); + /// Required Types tag not found. + internal const string TypesElementExpected = nameof(TypesElementExpected); + /// Content Types XML does not match schema. + internal const string TypesXmlDoesNotMatchSchema = nameof(TypesXmlDoesNotMatchSchema); + /// Default tag is not valid per the schema. Verify that attributes are correct. + internal const string DefaultTagDoesNotMatchSchema = nameof(DefaultTagDoesNotMatchSchema); + /// Override tag is not valid per the schema. Verify that attributes are correct. + internal const string OverrideTagDoesNotMatchSchema = nameof(OverrideTagDoesNotMatchSchema); + /// '{0}' element must be empty. + internal const string ElementIsNotEmptyElement = nameof(ElementIsNotEmptyElement); + /// Format error in package. + internal const string BadPackageFormat = nameof(BadPackageFormat); + /// Streaming mode is supported only for creating packages. + internal const string StreamingModeNotSupportedForConsumption = nameof(StreamingModeNotSupportedForConsumption); + /// Must have write-only access to produce a package in streaming mode. + internal const string StreamingPackageProductionImpliesWriteOnlyAccess = nameof(StreamingPackageProductionImpliesWriteOnlyAccess); + /// Cannot have concurrent write accesses on package being produced in streaming mode. + internal const string StreamingPackageProductionRequiresSingleWriter = nameof(StreamingPackageProductionRequiresSingleWriter); + /// '{0}' method can only be called on a package opened in streaming mode. + internal const string MethodAvailableOnlyInStreamingCreation = nameof(MethodAvailableOnlyInStreamingCreation); + /// Package.{0} is not supported in streaming production. + internal const string OperationIsNotSupportedInStreamingProduction = nameof(OperationIsNotSupportedInStreamingProduction); + /// Only write operations are supported in streaming production. + internal const string OnlyWriteOperationsAreSupportedInStreamingCreation = nameof(OnlyWriteOperationsAreSupportedInStreamingCreation); + /// Write-once semantics in streaming production precludes the use of '{0}'. + internal const string OperationViolatesWriteOnceSemantics = nameof(OperationViolatesWriteOnceSemantics); + /// Streaming consumption of packages not supported. + internal const string OnlyStreamingProductionIsSupported = nameof(OnlyStreamingProductionIsSupported); + /// Read or write operation references location outside the bounds of the buffer provided. + internal const string IOBufferOverflow = nameof(IOBufferOverflow); + /// Cannot change content of a read-only stream. + internal const string StreamDoesNotSupportWrite = nameof(StreamDoesNotSupportWrite); + /// Package has more than one Core Properties relationship. + internal const string MoreThanOneMetadataRelationships = nameof(MoreThanOneMetadataRelationships); + /// TargetMode for a Core Properties relationship must be 'Internal'. + internal const string NoExternalTargetForMetadataRelationship = nameof(NoExternalTargetForMetadataRelationship); + /// Unrecognized root element in Core Properties part. + internal const string CorePropertiesElementExpected = nameof(CorePropertiesElementExpected); + /// Core Properties part: core property elements can contain only text data. + internal const string NoStructuredContentInsideProperties = nameof(NoStructuredContentInsideProperties); + /// Unrecognized namespace in Core Properties part. + internal const string UnknownNamespaceInCorePropertiesPart = nameof(UnknownNamespaceInCorePropertiesPart); + /// '{0}' property name is not valid in Core Properties part. + internal const string InvalidPropertyNameInCorePropertiesPart = nameof(InvalidPropertyNameInCorePropertiesPart); + /// Core Properties part: A property start-tag was expected. + internal const string PropertyStartTagExpected = nameof(PropertyStartTagExpected); + /// Core Properties part: Text data of XSD type 'DateTime' was expected. + internal const string XsdDateTimeExpected = nameof(XsdDateTimeExpected); + /// The target of the Core Properties relationship does not reference an existing part. + internal const string DanglingMetadataRelationship = nameof(DanglingMetadataRelationship); + /// The Core Properties relationship references a part that has an incorrect content type. + internal const string WrongContentTypeForPropertyPart = nameof(WrongContentTypeForPropertyPart); + /// Unexpected number of attributes is found on '{0}'. + internal const string PropertyWrongNumbOfAttribsDefinedOn = nameof(PropertyWrongNumbOfAttribsDefinedOn); + /// Unknown xsi:type for DateTime on '{0}'. + internal const string UnknownDCDateTimeXsiType = nameof(UnknownDCDateTimeXsiType); + /// More than one '{0}' property found. + internal const string DuplicateCorePropertyName = nameof(DuplicateCorePropertyName); + /// PackageProperties object was disposed. + internal const string StorageBasedPackagePropertiesDiposed = nameof(StorageBasedPackagePropertiesDiposed); + /// Encoding format is not supported. Only UTF-8 and UTF-16 are supported. + internal const string EncodingNotSupported = nameof(EncodingNotSupported); + /// Duplicate pieces found in the package. + internal const string DuplicatePiecesFound = nameof(DuplicatePiecesFound); + /// Cannot find piece with the specified piece number. + internal const string PieceDoesNotExist = nameof(PieceDoesNotExist); + /// This serviceType is already registered to another service. + internal const string ServiceTypeAlreadyAdded = nameof(ServiceTypeAlreadyAdded); + /// '{0}' type name does not have the expected format 'className, assembly'. + internal const string QualifiedNameHasWrongFormat = nameof(QualifiedNameHasWrongFormat); + /// Too many attributes are specified for '{0}'. + internal const string ParserAttributeArgsHigh = nameof(ParserAttributeArgsHigh); + /// '{0}' requires more attributes. + internal const string ParserAttributeArgsLow = nameof(ParserAttributeArgsLow); + /// Cannot load assembly '{0}' because a different version of that same assembly is loaded '{1}'. + internal const string ParserAssemblyLoadVersionMismatch = nameof(ParserAssemblyLoadVersionMismatch); + /// (null) + internal const string ToStringNull = nameof(ToStringNull); + /// '{0}' ValueSerializer cannot convert '{1}' to '{2}'. + internal const string ConvertToException = nameof(ConvertToException); + /// '{0}' ValueSerializer cannot convert from '{1}'. + internal const string ConvertFromException = nameof(ConvertFromException); + /// SortDescription must have a nonempty property name. + internal const string SortDescriptionPropertyNameCannotBeEmpty = nameof(SortDescriptionPropertyNameCannotBeEmpty); + /// Cannot modify a '{0}' after it is sealed. + internal const string CannotChangeAfterSealed = nameof(CannotChangeAfterSealed); + /// Cannot group by property '{0}' because it cannot be found on type '{1}'. + internal const string BadPropertyForGroup = nameof(BadPropertyForGroup); + /// The CollectionView that originates this CurrentChanging event is in a state that does not allow the event to be canceled. Check CurrentChangingEventArgs.IsCancelable before assigning to this CurrentChangingEventArgs.Cancel property. + internal const string CurrentChangingCannotBeCanceled = nameof(CurrentChangingCannotBeCanceled); + /// Collection is read-only. + internal const string NotSupported_ReadOnlyCollection = nameof(NotSupported_ReadOnlyCollection); + /// Only single dimensional arrays are supported for the requested action. + internal const string Arg_RankMultiDimNotSupported = nameof(Arg_RankMultiDimNotSupported); + /// The lower bound of target array must be zero. + internal const string Arg_NonZeroLowerBound = nameof(Arg_NonZeroLowerBound); + /// Non-negative number required. + internal const string ArgumentOutOfRange_NeedNonNegNum = nameof(ArgumentOutOfRange_NeedNonNegNum); + /// Destination array is not long enough to copy all the items in the collection. Check array index and length. + internal const string Arg_ArrayPlusOffTooSmall = nameof(Arg_ArrayPlusOffTooSmall); + /// Target array type is not compatible with the type of items in the collection. + internal const string Argument_InvalidArrayType = nameof(Argument_InvalidArrayType); + /// '{0}' index is beyond maximum '{1}'. + internal const string ReachOutOfRange = nameof(ReachOutOfRange); + /// Permission state is not valid. + internal const string InvalidPermissionState = nameof(InvalidPermissionState); + /// Target is not a WebBrowserPermission. + internal const string TargetNotWebBrowserPermissionLevel = nameof(TargetNotWebBrowserPermissionLevel); + /// Target is not a MediaPermission. + internal const string TargetNotMediaPermissionLevel = nameof(TargetNotMediaPermissionLevel); + /// '{0}' attribute is not valid XML. + internal const string BadXml = nameof(BadXml); + /// Permission level is not valid. + internal const string InvalidPermissionLevel = nameof(InvalidPermissionLevel); + /// Choice is valid only in AlternateContent. + internal const string XCRChoiceOnlyInAC = nameof(XCRChoiceOnlyInAC); + /// Choice cannot follow a Fallback. + internal const string XCRChoiceAfterFallback = nameof(XCRChoiceAfterFallback); + /// Choice must contain Requires attribute. + internal const string XCRRequiresAttribNotFound = nameof(XCRRequiresAttribNotFound); + /// Requires attribute must contain a valid namespace prefix. + internal const string XCRInvalidRequiresAttribute = nameof(XCRInvalidRequiresAttribute); + /// Fallback is valid only in AlternateContent. + internal const string XCRFallbackOnlyInAC = nameof(XCRFallbackOnlyInAC); + /// AlternateContent must contain one or more Choice elements. + internal const string XCRChoiceNotFound = nameof(XCRChoiceNotFound); + /// AlternateContent must contain only one Fallback element. + internal const string XCRMultipleFallbackFound = nameof(XCRMultipleFallbackFound); + /// '{0}' attribute is not valid for '{1}' element. + internal const string XCRInvalidAttribInElement = nameof(XCRInvalidAttribInElement); + /// Unrecognized Compatibility element '{0}'. + internal const string XCRUnknownCompatElement = nameof(XCRUnknownCompatElement); + /// '{0}' element is not a valid child of AlternateContent. Only Choice and Fallback elements are valid children of an AlternateContent element. + internal const string XCRInvalidACChild = nameof(XCRInvalidACChild); + /// '{0}' format is not valid. + internal const string XCRInvalidFormat = nameof(XCRInvalidFormat); + /// '{0}' prefix is not defined. + internal const string XCRUndefinedPrefix = nameof(XCRUndefinedPrefix); + /// Unrecognized compatibility attribute '{0}'. + internal const string XCRUnknownCompatAttrib = nameof(XCRUnknownCompatAttrib); + /// '{0}' namespace cannot process content; it must be declared Ignorable first. + internal const string XCRNSProcessContentNotIgnorable = nameof(XCRNSProcessContentNotIgnorable); + /// Duplicate ProcessContent declaration for element '{1}' in namespace '{0}'. + internal const string XCRDuplicateProcessContent = nameof(XCRDuplicateProcessContent); + /// Cannot have both a specific and a wildcard ProcessContent declaration for namespace '{0}'. + internal const string XCRInvalidProcessContent = nameof(XCRInvalidProcessContent); + /// Duplicate wildcard ProcessContent declaration for namespace '{0}'. + internal const string XCRDuplicateWildcardProcessContent = nameof(XCRDuplicateWildcardProcessContent); + /// MustUnderstand condition failed on namespace '{0}' + internal const string XCRMustUnderstandFailed = nameof(XCRMustUnderstandFailed); + /// '{0}' namespace cannot preserve items; it must be declared Ignorable first. + internal const string XCRNSPreserveNotIgnorable = nameof(XCRNSPreserveNotIgnorable); + /// Duplicate Preserve declaration for element {1} in namespace '{0}'. + internal const string XCRDuplicatePreserve = nameof(XCRDuplicatePreserve); + /// Cannot have both a specific and a wildcard Preserve declaration for namespace '{0}'. + internal const string XCRInvalidPreserve = nameof(XCRInvalidPreserve); + /// Duplicate wildcard Preserve declaration for namespace '{0}'. + internal const string XCRDuplicateWildcardPreserve = nameof(XCRDuplicateWildcardPreserve); + /// '{0}' attribute value is not a valid XML name. + internal const string XCRInvalidXMLName = nameof(XCRInvalidXMLName); + /// There is a cycle of XML compatibility definitions, such that namespace '{0}' overrides itself. This could be due to inconsistent XmlnsCompatibilityAttributes in different assemblies. Please change the definitions to eliminate this cycle. + internal const string XCRCompatCycle = nameof(XCRCompatCycle); + /// '{1}' event not found on type '{0}'. + internal const string EventNotFound = nameof(EventNotFound); + /// Listener did not handle requested event. + internal const string ListenerDidNotHandleEvent = nameof(ListenerDidNotHandleEvent); + /// Listener of type '{0}' registered with event manager of type '{1}', but then did not handle the event. The listener is coded incorrectly. + internal const string ListenerDidNotHandleEventDetail = nameof(ListenerDidNotHandleEventDetail); + /// WeakEventManager supports only delegates with one target. + internal const string NoMulticastHandlers = nameof(NoMulticastHandlers); + /// Unrecoverable system error. + internal const string InvariantFailure = nameof(InvariantFailure); + /// ContentType string cannot have leading/trailing Linear White Spaces [LWS - RFC 2616]. + internal const string ContentTypeCannotHaveLeadingTrailingLWS = nameof(ContentTypeCannotHaveLeadingTrailingLWS); + /// ContentType string is not valid. Expected format is type/subtype. + internal const string InvalidTypeSubType = nameof(InvalidTypeSubType); + /// ';' must be followed by parameter=value pair. + internal const string ExpectingParameterValuePairs = nameof(ExpectingParameterValuePairs); + /// Parameter and value pair is not valid. Expected form is parameter=value. + internal const string InvalidParameterValuePair = nameof(InvalidParameterValuePair); + /// A token is not valid. Refer to RFC 2616 for correct grammar of content types. + internal const string InvalidToken = nameof(InvalidToken); + /// Parameter value must be a valid token or a quoted string as per RFC 2616. + internal const string InvalidParameterValue = nameof(InvalidParameterValue); + /// A Linear White Space character is not valid. + internal const string InvalidLinearWhiteSpaceCharacter = nameof(InvalidLinearWhiteSpaceCharacter); + /// Semicolon separator is required between two valid parameter=value pairs. + internal const string ExpectingSemicolon = nameof(ExpectingSemicolon); + /// HwndSubclass.Attach has already been called; it cannot be called again. + internal const string HwndSubclassMultipleAttach = nameof(HwndSubclassMultipleAttach); + /// Cannot locate resource '{0}'. + internal const string UnableToLocateResource = nameof(UnableToLocateResource); + /// Please wait while the application opens + internal const string SplashScreenIsLoading = nameof(SplashScreenIsLoading); + /// Name cannot be an empty string. + internal const string NameScopeNameNotEmptyString = nameof(NameScopeNameNotEmptyString); + /// '{0}' Name is not found. + internal const string NameScopeNameNotFound = nameof(NameScopeNameNotFound); + /// Cannot register duplicate Name '{0}' in this scope. + internal const string NameScopeDuplicateNamesNotAllowed = nameof(NameScopeDuplicateNamesNotAllowed); + /// No NameScope found to {1} the Name '{0}'. + internal const string NameScopeNotFound = nameof(NameScopeNotFound); + /// '{0}' name is not valid for identifier. + internal const string NameScopeInvalidIdentifierName = nameof(NameScopeInvalidIdentifierName); + /// No dependency property {0} on {1}. + internal const string NoDependencyProperty = nameof(NoDependencyProperty); + /// Must set ArrayType before calling ProvideValue on ArrayExtension. + internal const string MarkupExtensionArrayType = nameof(MarkupExtensionArrayType); + /// Items in the array must be type '{0}'. One or more items cannot be cast to this type. + internal const string MarkupExtensionArrayBadType = nameof(MarkupExtensionArrayBadType); + /// Markup extension '{0}' requires '{1}' be implemented in the IServiceProvider for ProvideValue. + internal const string MarkupExtensionNoContext = nameof(MarkupExtensionNoContext); + /// '{0}' StaticExtension value cannot be resolved to an enumeration, static field, or static property. + internal const string MarkupExtensionBadStatic = nameof(MarkupExtensionBadStatic); + /// StaticExtension must have Member property set before ProvideValue can be called. + internal const string MarkupExtensionStaticMember = nameof(MarkupExtensionStaticMember); + /// TypeExtension must have TypeName property set before ProvideValue can be called. + internal const string MarkupExtensionTypeName = nameof(MarkupExtensionTypeName); + /// '{0}' string is not valid for type. + internal const string MarkupExtensionTypeNameBad = nameof(MarkupExtensionTypeNameBad); + /// '{0}' must be of type '{1}'. + internal const string MustBeOfType = nameof(MustBeOfType); + /// This operation requires the thread's apartment state to be '{0}'. + internal const string Verify_ApartmentState = nameof(Verify_ApartmentState); + /// The argument can neither be null nor empty. + internal const string Verify_NeitherNullNorEmpty = nameof(Verify_NeitherNullNorEmpty); + /// The argument can not be equal to '{0}'. + internal const string Verify_AreNotEqual = nameof(Verify_AreNotEqual); + /// No file exists at '{0}'. + internal const string Verify_FileExists = nameof(Verify_FileExists); + /// Event argument is invalid. + internal const string InvalidEvent = nameof(InvalidEvent); + /// The property '{0}' cannot be changed. The '{1}' class has been sealed. + internal const string CompatibilityPreferencesSealed = nameof(CompatibilityPreferencesSealed); + /// Desktop applications are required to opt in to all earlier accessibility improvements to get the later improvements. To do this, ensure that if the AppContext switch 'Switch.UseLegacyAccessibilityFeatures.N' is set to 'false', then 'Switch.UseLegacyAccessi ... + internal const string CombinationOfAccessibilitySwitchesNotSupported = nameof(CombinationOfAccessibilitySwitchesNotSupported); + /// Desktop applications setting AppContext switch '{0}' to false are required to opt in to all earlier accessibility improvements. To do this, ensure that the AppContext switch '{1}' is set to 'false', then 'Switch.UseLegacyAccessibilityFeatures' and all 'Swi ... + internal const string AccessibilitySwitchDependencyNotSatisfied = nameof(AccessibilitySwitchDependencyNotSatisfied); + /// Extra data encountered at position {0} while parsing '{1}'. + internal const string TokenizerHelperExtraDataEncountered = nameof(TokenizerHelperExtraDataEncountered); + /// Premature string termination encountered while parsing '{0}'. + internal const string TokenizerHelperPrematureStringTermination = nameof(TokenizerHelperPrematureStringTermination); + /// Missing end quote encountered while parsing '{0}'. + internal const string TokenizerHelperMissingEndQuote = nameof(TokenizerHelperMissingEndQuote); + /// Empty token encountered at position {0} while parsing '{1}'. + internal const string TokenizerHelperEmptyToken = nameof(TokenizerHelperEmptyToken); + /// No current object to return. + internal const string Enumerator_VerifyContext = nameof(Enumerator_VerifyContext); + /// PermissionState value '{0}' is not valid for this Permission. + internal const string InvalidPermissionStateValue = nameof(InvalidPermissionStateValue); + /// Permission type is not valid. Expected '{0}'. + internal const string InvalidPermissionType = nameof(InvalidPermissionType); + /// Parameter cannot be a zero-length string. + internal const string StringEmpty = nameof(StringEmpty); + /// Parameter must be greater than or equal to zero. + internal const string ParameterCannotBeNegative = nameof(ParameterCannotBeNegative); + /// Specified value of type '{0}' must have IsFrozen set to false to modify. + internal const string Freezable_CantBeFrozen = nameof(Freezable_CantBeFrozen); + /// Cannot change property metadata after it has been associated with a property. + internal const string TypeMetadataCannotChangeAfterUse = nameof(TypeMetadataCannotChangeAfterUse); + /// '{0}' enumeration value is not valid. + internal const string Enum_Invalid = nameof(Enum_Invalid); + /// Cannot convert string value '{0}' to type '{1}'. + internal const string CannotConvertStringToType = nameof(CannotConvertStringToType); + /// Cannot modify a read-only container. + internal const string CannotModifyReadOnlyContainer = nameof(CannotModifyReadOnlyContainer); + /// Cannot get part or part information from a write-only container. + internal const string CannotRetrievePartsOfWriteOnlyContainer = nameof(CannotRetrievePartsOfWriteOnlyContainer); + /// '{0}' file does not conform to the expected file format specification. + internal const string FileFormatExceptionWithFileName = nameof(FileFormatExceptionWithFileName); + /// Input file or data stream does not conform to the expected file format specification. + internal const string FileFormatException = nameof(FileFormatException); + /// {0} is an invalid handle. + internal const string Cryptography_InvalidHandle = nameof(Cryptography_InvalidHandle); + /// DLL Name: {0} DLL Location: {1} + internal const string WpfDllConsistencyErrorData = nameof(WpfDllConsistencyErrorData); + /// Failed WPF DLL consistency checks. Expected location: {0}. + internal const string WpfDllConsistencyErrorHeader = nameof(WpfDllConsistencyErrorHeader); + + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/SR.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/SR.cs new file mode 100644 index 0000000..9e81ad7 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/WpfInkRenderAlgorithms/ref/SR.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace System; + +internal static partial class SR +{ + public static string? Size_CannotModifyEmptySize; + public static string? Rect_CannotCallMethod; + public static string? Size_HeightCannotBeNegative; + public static string? Size_WidthCannotBeNegative; + public static string? Rect_CannotModifyEmptyRect; + public static string? Color_NullColorContext; + public static string? Color_DimensionMismatch; + public static string? Stylus_InvalidMax; + public static Exception? InvalidStylusPointXYNaN; + public static string? Size_WidthAndHeightCannotBeNegative; + public static string? InvalidStylusPointDescription { get; set; } + public static string? InvalidStylusPointDescriptionDuplicatesFound { get; set; } + public static Exception? InvalidPressureValue { get; set; } + public static string? InvalidAdditionalDataForStylusPoint { get; set; } + public static string? InvalidStylusPointProperty { get; set; } + public static Exception? InvalidMinMaxForButton { get; set; } + public static string? InvalidStylusPointConstructionZeroLengthCollection { get; set; } + public static string? IncompatibleStylusPointDescriptions { get; set; } + public static string? InvalidStylusPointCollectionZeroCount { get; set; } + public static string? InvalidStylusPointDescriptionButtonsMustBeLast { get; set; } + public static string? InvalidStylusPointDescriptionTooManyButtons { get; set; } + public static string? InvalidIsButtonForId { get; set; } + public static string? InvalidIsButtonForId2 { get; set; } + public static string? InvalidStylusPointPropertyInfoResolution { get; set; } + + public static string Get(string invalidGuid, params object[] p) + { + return string.Empty; + } + + public static string? Format(string? a, params object[] p) + { + return a; + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/AssemblyInfo.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/AssemblyInfo.cs new file mode 100644 index 0000000..39be099 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("DotNetCampus.InkCanvas.X11InkCanvas")] +[assembly: InternalsVisibleTo("DotNetCampus.AvaloniaInkCanvas")] \ No newline at end of file diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Context_/SkiaStrokeSynchronizer.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Context_/SkiaStrokeSynchronizer.cs new file mode 100644 index 0000000..d049b66 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Context_/SkiaStrokeSynchronizer.cs @@ -0,0 +1,24 @@ +using SkiaSharp; +using InkStylusPoint = DotNetCampus.Inking.Primitive.InkStylusPoint; + +namespace DotNetCampus.Inking; + +/// +/// 笔迹信息 用于静态笔迹层 +/// +record SkiaStrokeSynchronizer( + uint StylusDeviceId, + InkId InkId, + SKColor StrokeColor, + double StrokeInkThickness, + SKPath? InkStrokePath, + List StylusPoints)// : InkSynchronizer.StrokeSynchronizer(StylusDeviceId) +{ + public SKRect GetBounds() + { + return _bounds ??= InkStrokePath?.Bounds ?? SKRect.Empty; + } + + private SKRect? _bounds; +} +; diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/DotNetCampus.InkCanvas.SkiaInk.csproj b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/DotNetCampus.InkCanvas.SkiaInk.csproj new file mode 100644 index 0000000..d598af6 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/DotNetCampus.InkCanvas.SkiaInk.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + DotNetCampus.Inking + true + true + + + + + + + + + + + diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/DotNetCampus.InkCanvas.SkiaInk.csproj.DotSettings b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/DotNetCampus.InkCanvas.SkiaInk.csproj.DotSettings new file mode 100644 index 0000000..8616cc0 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/DotNetCampus.InkCanvas.SkiaInk.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/EraserView.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/EraserView.cs new file mode 100644 index 0000000..530073d --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/EraserView.cs @@ -0,0 +1,51 @@ +using SkiaSharp; + +namespace DotNetCampus.Inking; + +class EraserView +{ + public SKBitmap GetEraserView(int width, int height) + { + var skBitmap = new SKBitmap(new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul)); + + using var skCanvas = new SKCanvas(skBitmap); + DrawEraserView(skCanvas, width, height); + + return skBitmap; + } + + public void DrawEraserView(SKCanvas skCanvas, float width, float height) + { + var pathWidth = 30; + var pathHeight = 45; + + bool needScale = Math.Abs(width - pathWidth) > 0.001 || Math.Abs(height - pathHeight) > 0.001; + + if (needScale) + { + skCanvas.Save(); + skCanvas.Scale(width / pathWidth, height / pathHeight); + } + + using var path1 = SKPath.ParseSvgPathData( + "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"); + using var skPaint = new SKPaint(); + skPaint.IsAntialias = true; + skPaint.Style = SKPaintStyle.Fill; + skPaint.Color = new SKColor(0, 0, 0, 0x33); + skCanvas.DrawPath(path1, skPaint); + + skPaint.Color = new SKColor(0xF2, 0xEE, 0xEB, 0xFF); + skCanvas.DrawRoundRect(1, 1, 28, 43, 4, 4, skPaint); + + using var path2 = SKPath.ParseSvgPathData( + "M20,29.1666667L20,16.1666667C20,15.3382395 19.3284271,14.6666667 18.5,14.6666667 17.6715729,14.6666667 17,15.3382395 17,16.1666667L17,29.1666667C17,29.9950938 17.6715729,30.6666667 18.5,30.6666667 19.3284271,30.6666667 20,29.9950938 20,29.1666667z M13,29.1666667L13,16.1666667C13,15.3382395 12.3284271,14.6666667 11.5,14.6666667 10.6715729,14.6666667 10,15.3382395 10,16.1666667L10,29.1666667C10,29.9950938 10.6715729,30.6666667 11.5,30.6666667 12.3284271,30.6666667 13,29.9950938 13,29.1666667z"); + skPaint.Color = new SKColor(0x00, 0x00, 0x00, 0x26); + skCanvas.DrawPath(path2, skPaint); + + if (needScale) + { + skCanvas.Restore(); + } + } +} \ No newline at end of file diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Erasing/ErasingCompletedEventArgs.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Erasing/ErasingCompletedEventArgs.cs new file mode 100644 index 0000000..135ffcc --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Erasing/ErasingCompletedEventArgs.cs @@ -0,0 +1,28 @@ +namespace DotNetCampus.Inking.Erasing; + +/// +/// 擦除完成参数 +/// +class SkInkCanvasErasingCompletedEventArgs : EventArgs +{ + public SkInkCanvasErasingCompletedEventArgs(bool isCanceled) + { + IsCanceled = isCanceled; + } + + public SkInkCanvasErasingCompletedEventArgs(IReadOnlyList originList, + IReadOnlyList newList) : this(false) + { + OriginList = originList; + NewList = newList; + } + + public IReadOnlyList OriginList { get; } = null!; + + public IReadOnlyList NewList { get; } = null!; + + /// + /// 是否取消 + /// + public bool IsCanceled { get; private set; } +} \ No newline at end of file diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Interactives/SkInkCanvasManipulationManager.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Interactives/SkInkCanvasManipulationManager.cs new file mode 100644 index 0000000..c1b955c --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Interactives/SkInkCanvasManipulationManager.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DotNetCampus.Inking.Interactives; + +enum InputMode +{ + Ink, + Manipulate, +} + + +internal class SkInkCanvasManipulationManager +{ +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Primitive/RectExtension.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Primitive/RectExtension.cs new file mode 100644 index 0000000..1235256 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Primitive/RectExtension.cs @@ -0,0 +1,118 @@ +using SkiaSharp; +using Rect = DotNetCampus.Numerics.Geometry.Rect2D; + +namespace DotNetCampus.Inking.Primitive; + +static class RectExtension +{ + public static SKRectI LimitRect(SKRectI inputRect, SKRectI maxRect) + { + var left = inputRect.Left; + var top = inputRect.Top; + var right = inputRect.Right; + var bottom = inputRect.Bottom; + + left = Math.Max(left, maxRect.Left); + top = Math.Max(top, maxRect.Top); + right = Math.Min(right, maxRect.Right); + bottom = Math.Min(bottom, maxRect.Bottom); + + var width = right - left; + var height = bottom - top; + + if (width <= 0 || height <= 0) + { + return SKRectI.Empty; + } + + return SKRectI.Create(left, top, width, height); + } + + public static SKRect LimitRect(SKRect inputRect, SKRect maxRect) + { + var left = inputRect.Left; + var top = inputRect.Top; + var right = inputRect.Right; + var bottom = inputRect.Bottom; + + left = Math.Max(left, maxRect.Left); + top = Math.Max(top, maxRect.Top); + right = Math.Min(right, maxRect.Right); + bottom = Math.Min(bottom, maxRect.Bottom); + + var width = right - left; + var height = bottom - top; + + if (width <= 0 || height <= 0) + { + return SKRect.Empty; + } + + return SKRect.Create(left, top, width, height); + } + + public static Rect LimitRect(Rect inputRect, Rect maxRect) + { + var left = inputRect.Left; + var top = inputRect.Top; + var right = inputRect.Right; + var bottom = inputRect.Bottom; + + if (double.IsNaN(left) || double.IsNaN(top) || double.IsNaN(right) || double.IsNaN(bottom)) + { + return Rect.Zero; + } + + left = Math.Max(left, maxRect.Left); + top = Math.Max(top, maxRect.Top); + right = Math.Min(right, maxRect.Right); + bottom = Math.Min(bottom, maxRect.Bottom); + + var width = right - left; + var height = bottom - top; + + if (width <= 0 || height <= 0) + { + return Rect.Zero; + } + + return new Rect(left, top, width, height); + } + + public static Rect ExpandLength(SKRect rect, double additionLengthIncrement) + { + return new Rect(rect.Left - additionLengthIncrement, rect.Top - additionLengthIncrement, + rect.Width + additionLengthIncrement * 2, rect.Height + additionLengthIncrement * 2); + } + + public static SKRect ExpandSKRectLength(SKRect rect, float additionLengthIncrement) + { + return new SKRect(rect.Left - additionLengthIncrement, rect.Top - additionLengthIncrement, + rect.Width + additionLengthIncrement * 2, rect.Height + additionLengthIncrement * 2); + } + + //public static Rect ToMauiRect(this SKRect rect) + //{ + // return new Rect(rect.Left, rect.Top, rect.Width, rect.Height); + //} + + //public static Rect ToMauiRect(this Rect rect) + //{ + // return new Rect(rect.Left, rect.Top, rect.Width, rect.Height); + //} + + public static Rect ToRect2D(this SKRect rect) + { + return new Rect(rect.Left, rect.Top, rect.Width, rect.Height); + } + + public static Rect ToRect2D(this Rect rect) + { + return new Rect(rect.Left, rect.Top, rect.Width, rect.Height); + } + + public static SKRect ToSkRect(this Rect rect) + { + return new SKRect((float) rect.Left, (float) rect.Top, (float) rect.Right, (float) rect.Bottom); + } +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/CleanStrokeSettings.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/CleanStrokeSettings.cs new file mode 100644 index 0000000..4ce710a --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/CleanStrokeSettings.cs @@ -0,0 +1,17 @@ +namespace DotNetCampus.Inking.Settings; + +/// +/// 对清空笔迹的配置 +/// +record CleanStrokeSettings +{ + /// + /// 清空笔迹之后,需要绘制背景图。对于一些背景是没有任何内容的应用,则不需要绘制,提升性能。因为清空笔迹之后,会将当前的静态笔迹都绘制一次,除非背景有图片或其他内容,否则不需要绘制背景 + /// + public bool ShouldDrawBackground { get; init; } = false; + + /// + /// 清空笔迹之后,是否需要更新背景图。解决当前有两个笔迹,只删除其中一个笔迹,如果此时背景没有更新,则可能导致两个笔迹都被删除,或被删除的笔迹依然在背景里 + /// + public bool ShouldUpdateBackground { get; init; } = true; +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/DropPointSettings.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/DropPointSettings.cs new file mode 100644 index 0000000..367e1c3 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/DropPointSettings.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DotNetCampus.Inking.Settings; + +/// +/// 丢点配置 +/// +record DropPointSettings +{ + /// + /// 最大丢点数量 + /// + public int MaxDropPointCount { get; init; } = 10; + + public int MaxDistanceLength { get; init; } = 2; + + /// + /// 丢点策略 + /// + public DropPointStrategy DropPointStrategy { get; init; } = DropPointStrategy.Normal; +} + +/// +/// 丢点策略 +/// +public enum DropPointStrategy +{ + /// + /// 普通的丢点,不会丢太多 + /// + Normal, + + /// + /// 激进策略,会丢很多点 + /// + Aggressive, +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/InkCanvasDynamicRenderTipStrokeType.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/InkCanvasDynamicRenderTipStrokeType.cs new file mode 100644 index 0000000..6c86516 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/InkCanvasDynamicRenderTipStrokeType.cs @@ -0,0 +1,28 @@ +using DotNetCampus.Inking.Utils; + +namespace DotNetCampus.Inking.Settings; + +/// +/// 笔尖渲染模式 +/// +enum InkCanvasDynamicRenderTipStrokeType +{ + /// + /// 通过裁剪画布的方式进行绘制所有的笔迹 + /// + /// 这是当前最快的笔迹,写的快炸的快 + /// 这里面用了绕过 Skia 的裁剪,使用 替换为背景 + RenderAllTouchingStrokeWithClip, + + /// + /// 所有触摸按下的笔迹都每次重新绘制,不区分笔尖和笔身 + /// 此方式可以实现比较好的平滑效果 + /// + /// 此方式性能比较差,但是最符合预期的 + RenderAllTouchingStrokeWithoutTipStroke, + + /// + /// 只渲染笔尖部分 + /// + RenderTipStrokeOnly, +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/InkCanvasEraserAlgorithmMode.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/InkCanvasEraserAlgorithmMode.cs new file mode 100644 index 0000000..36310b5 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/InkCanvasEraserAlgorithmMode.cs @@ -0,0 +1,31 @@ +namespace DotNetCampus.Inking.Settings; + +/// +/// 橡皮擦算法模式 +/// +enum InkCanvasEraserAlgorithmMode +{ + /// + /// 是否允许使用裁剪方式的橡皮擦,而不是走静态笔迹层。使用裁剪而不是使用笔迹计算,将笔迹的点给去掉 + /// + EnableClippingEraser, + + /// + /// 是否允许使用裁剪方式的橡皮擦,橡皮擦每次裁剪都写入画布,需要有多余的画布拷贝逻辑,但是不需要做 Path 处理。原理同 但是具体的裁剪逻辑不相同。用来减少擦除时间长时的越擦越卡的问题 + /// 此模式不支持漫游画布和切页。因为漫游画布和切页需要 Path 处理,而此模式没有进行 Path 处理,只是进行位图处理 + /// + /// 非路径 Path 的点擦情况下,当前最优的橡皮擦算法 + EnableClippingEraserWithoutEraserPathCombine, + + /// + /// 是否允许使用裁剪方式的橡皮擦,带不安全模式的二进制,橡皮擦每次裁剪都写入画布,需要有多余的画布拷贝逻辑,但是不需要做 Path 处理。原理同 但是具体的裁剪逻辑不相同。用来减少擦除时间长时的越擦越卡的问题。带不安全的二进制处理可以提升画图片的性能 + /// + /// 尚未全部完成 + EnableClippingEraserWithBinaryWithoutEraserPathCombine, + + /// + /// 进行点和 Path 的命中测试的橡皮擦,真实擦掉笔迹点和 Path 的橡皮擦。移动过程走 算法,抬手使用点命中擦掉笔迹点的算法 + /// + /// 路径 Path 的点擦情况下,当前最优的橡皮擦算法 + EnablePointPathEraser, +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/SkInkCanvasSettings.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/SkInkCanvasSettings.cs new file mode 100644 index 0000000..2fa132f --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Settings/SkInkCanvasSettings.cs @@ -0,0 +1,118 @@ +using DotNetCampus.Inking.Primitive; +using SkiaSharp; +using Size = DotNetCampus.Numerics.Geometry.Size2D; + +namespace DotNetCampus.Inking.Settings; + +/// +/// 画板的配置 +/// +/// 是否开启自动软笔模式 +record SkInkCanvasSettings(bool AutoSoftPen = true) +{ + public InkCanvasEraserAlgorithmMode EraserMode { init; get; } = + InkCanvasEraserAlgorithmMode.EnableClippingEraserWithoutEraserPathCombine; + + /// + /// 修改笔尖渲染部分配置 动态笔迹层 + /// + public InkCanvasDynamicRenderTipStrokeType DynamicRenderType { init; get; } = + InkCanvasDynamicRenderTipStrokeType.RenderAllTouchingStrokeWithoutTipStroke; + + /// + /// 是否应该在橡皮擦丢点进行收集,进行一次性处理。现在橡皮擦速度慢在画图 DrawBitmap 里,而对于几何组装来说,似乎不耗时。此属性可能会降低性能 + /// + /// 在触摸屏测试,使用兆芯机器,开启之后性能大幅降低 + public bool ShouldCollectDropErasePoint { init; get; } = true; + + /// + /// 笔迹颜色 + /// + public SKColor Color { get; init; } = SKColors.Red; + + /// + /// 笔迹粗细 + /// + public double InkThickness { get; init; } = 20; + + /// + /// 橡皮擦尺寸,可以在业务层,在手势橡皮擦过程中更改 + /// + public Size EraserSize { get; init; } = DefaultEraserSize; + + /// + /// 将触摸尺寸当成橡皮擦尺寸,即橡皮擦大小不完全跟随 尺寸,而是会根据 的触摸大小决定 + /// + public bool EnableStylusSizeAsEraserSize { get; init; } = true; + + /// + /// 橡皮擦是否可以一直按照触摸尺寸修改橡皮擦尺寸。属于演示效果较好,实际使用效果差。仅当 为 true 时此属性才有效。为 false 时,将在超过 时间,设置为最后的触摸面积固定大小,即只允许在开始擦的时候根据触摸面积修改大小,之后将固定大小 + /// + public bool CanEraserAlwaysFollowsTouchSize { init; get; } = false; + + /// + /// 橡皮擦可以根据触摸面积尺寸修改橡皮擦大小的时间。如果 为 true 则此属性无效。仅当 为 true 时此属性才有效 + /// + public TimeSpan EraserCanResizeDuringTimeSpan { init; get; } = TimeSpan.FromMilliseconds(600); + + /// + /// 默认的橡皮擦尺寸 + /// + /// 在 Paint DefaultEraserSize 是 48x72 大小 + public static Size DefaultEraserSize => new Size(30, 45); + + /// + /// 是否锁定最小橡皮擦尺寸,即要求橡皮擦尺寸最小为 大小 + /// + public bool LockMinEraserSize { init; get; } = true; + + /// + /// 最小橡皮擦尺寸。仅当 为 true 时生效 + /// + public Size MinEraserSize { init; get; } = new Size(48, 72); + + /// + /// 橡皮擦丢点时间,在这个时间内的连续输入将会被丢掉 + /// + public TimeSpan EraserDropPointTimeSpan { get; init; } = TimeSpan.FromMilliseconds(20); + + /// + /// 最小的橡皮擦手势尺寸,用于判断是否进入手势模式 + /// + /// 和 不同的是,此属性是像素单位 + public Size MinEraserGesturePixelSize { get; init; } = new Size(30, 45); + + /// + /// 最小的橡皮擦手势尺寸,物理尺寸,单位厘米 + /// + /// 据说大家的手都是 6 厘米,也不知道是谁说的 + public double MinEraserGesturePhysicalSizeCm { get; init; } = 6; + + /// + /// 在开始输入多久之后,就不能再进入橡皮擦了 + /// + /// 这是一个弱约定,上层业务方可取此属性判断,也可以强行进入手势橡皮擦模式 + public TimeSpan DisableEnterEraserGestureAfterInputDuring { get; init; } = TimeSpan.FromSeconds(1); + + /// + /// 是否启用手势橡皮擦 默认不启用,由上层业务自己调用进入手势橡皮擦模式。因为在这一层不好进行计算 + /// 此属性设置为 false 之后,需要上层业务自行决定什么时机进入手势橡皮擦模式,通过调用 EnterEraserMode 方法进入手势橡皮擦模式 + /// 此属性设置为 true 将会在框架层,通过输入的 的触摸尺寸,通过像素判断方法,判断是否大于 尺寸决定是否进入橡皮擦模式。由于通过像素方式判断不靠谱,因此推荐不要开启此属性。业务层自己决定更好 + /// + public bool EnableEraserGesture { get; init; } = false; + + /// + /// 是否忽略压感。 + /// + public bool IgnorePressure { get; init; } = true; + + /// + /// 是否在按下时需要调用 DrawStroke 方法,用于解决丢失按下的点 + /// + public bool ShouldDrawStrokeOnDown { get; init; } = false; + + /// + /// 清空笔迹的配置 + /// + public CleanStrokeSettings CleanStrokeSettings { get; init; } = new CleanStrokeSettings(); +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/SkiaSimpleInkRender.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/SkiaSimpleInkRender.cs new file mode 100644 index 0000000..959fe27 --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/SkiaSimpleInkRender.cs @@ -0,0 +1,174 @@ +using DotNetCampus.Inking.Primitive; + +using SkiaSharp; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Text; +using System.Threading.Tasks; + +namespace DotNetCampus.Inking; + +/// +/// 特别简单的笔迹渲染器。 +/// +internal class SkiaSimpleInkRender +{ + 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 SKPoint[] GetOutlineSKPointList(IReadOnlyList pointList, double inkSize) + { + if (pointList.Count < 2) + { + throw new ArgumentException("小于两个点的无法应用算法"); + } + + var outlinePointList1 = _outlinePointList1; + var outlinePointList2 = _outlinePointList2; + + outlinePointList1.Clear(); + outlinePointList2.Clear(); + + outlinePointList1.EnsureCapacity(pointList.Count * 2); + outlinePointList2.EnsureCapacity(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 SKPoint((float) (pointList[i].Point.X - direction.Y), (float) (pointList[i].Point.Y + direction.X)); + var point2 = new SKPoint((float) (pointList[i].Point.X + direction.Y), (float) (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 SKPoint((float) (pointList[i].Point.X + direction0.X), (float) (pointList[i].Point.Y + direction0.Y))); + outlinePointList1.Add(new SKPoint((float) (pointList[i].Point.X + directionN1.X), (float) (pointList[i].Point.Y + directionN1.Y))); + outlinePointList1.Add(new SKPoint((float) (pointList[i].Point.X + directionN2.X), (float) (pointList[i].Point.Y + directionN2.Y))); + outlinePointList1.Add(new SKPoint((float) (pointList[i].Point.X + directionN3.X), (float) (pointList[i].Point.Y + directionN3.Y))); + + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + direction0.X), (float) (pointList[i].Point.Y + direction0.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + direction1.X), (float) (pointList[i].Point.Y + direction1.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + direction2.X), (float) (pointList[i].Point.Y + direction2.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + direction3.X), (float) (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 SKPoint((float) (pointList[i].Point.X - direction.Y), (float) (pointList[i].Point.Y + direction.X)); + var point2 = new SKPoint((float) (pointList[i].Point.X + direction.Y), (float) (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 SKPoint((float) (pointList[i].Point.X + direction3.X), (float) (pointList[i].Point.Y + direction3.Y))); + outlinePointList1.Add(new SKPoint((float) (pointList[i].Point.X + direction2.X), (float) (pointList[i].Point.Y + direction2.Y))); + outlinePointList1.Add(new SKPoint((float) (pointList[i].Point.X + direction1.X), (float) (pointList[i].Point.Y + direction1.Y))); + outlinePointList1.Add(new SKPoint((float) (pointList[i].Point.X + direction0.X), (float) (pointList[i].Point.Y + direction0.Y))); + + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + directionN3.X), (float) (pointList[i].Point.Y + directionN3.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + directionN2.X), (float) (pointList[i].Point.Y + directionN2.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + directionN1.X), (float) (pointList[i].Point.Y + directionN1.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + direction0.X), (float) (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 SKPoint((float) (pointList[i].Point.X + vector1.X), (float) (pointList[i].Point.Y + vector1.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + vector12.X), (float) (pointList[i].Point.Y + vector12.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + vector2.X), (float) (pointList[i].Point.Y + vector2.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + vector22.X), (float) (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 SKPoint((float) (pointList[i].Point.X + vector11.X), (float) (pointList[i].Point.Y + vector11.Y))); + outlinePointList1.Add(new SKPoint((float) (pointList[i].Point.X + vector1.X), (float) (pointList[i].Point.Y + vector1.Y))); + outlinePointList1.Add(new SKPoint((float) (pointList[i].Point.X + vector21.X), (float) (pointList[i].Point.Y + vector21.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + vector2.X), (float) (pointList[i].Point.Y + vector2.Y))); + break; + } + default: + outlinePointList1.Add(new SKPoint((float) (pointList[i].Point.X + vector11.X), (float) (pointList[i].Point.Y + vector11.Y))); + outlinePointList1.Add(new SKPoint((float) (pointList[i].Point.X + vector21.X), (float) (pointList[i].Point.Y + vector21.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + vector12.X), (float) (pointList[i].Point.Y + vector12.Y))); + outlinePointList2.Add(new SKPoint((float) (pointList[i].Point.X + vector22.X), (float) (pointList[i].Point.Y + vector22.Y))); + break; + } + } + } + + var outlinePoints = new SKPoint[outlinePointList1.Count + outlinePointList2.Count + 1]; + outlinePointList2.Reverse(); + outlinePointList1.CopyTo(outlinePoints, 0); + outlinePointList2.CopyTo(outlinePoints, outlinePointList1.Count); + outlinePoints[^1] = outlinePoints[0]; + return outlinePoints; + } + + private readonly List _outlinePointList1 = new List(); + private readonly List _outlinePointList2 = new List(); +} diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Utils/SkiaExtension.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Utils/SkiaExtension.cs new file mode 100644 index 0000000..73b423c --- /dev/null +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.SkiaInk/Utils/SkiaExtension.cs @@ -0,0 +1,96 @@ +using System.Runtime.CompilerServices; +using SkiaSharp; + +namespace DotNetCampus.Inking.Utils; + +static class SkiaExtension +{ + /// + /// 从 拷贝所有像素覆盖原本的像素 + /// + /// + /// + /// + public static unsafe bool ReplacePixels(this SKBitmap destinationBitmap, SKBitmap sourceBitmap) + { + var destinationPixelPtr = (byte*) destinationBitmap.GetPixels(out var length).ToPointer(); + var sourcePixelPtr = (byte*) sourceBitmap.GetPixels().ToPointer(); + + Unsafe.CopyBlockUnaligned(destinationPixelPtr, sourcePixelPtr, (uint) length); + return true; + } + + /// + /// 从 拷贝指定范围 像素过来覆盖指定范围 的像素 + /// + /// + /// + /// + public static unsafe bool ReplacePixels(this SKBitmap destinationBitmap, SKBitmap sourceBitmap, SKRectI rect) + { + uint* basePtr = (uint*) destinationBitmap.GetPixels().ToPointer(); + uint* sourcePtr = (uint*) sourceBitmap.GetPixels().ToPointer(); + //Console.WriteLine($"ReplacePixels Rect={rect.Left},{rect.Top},{rect.Right},{rect.Bottom} wh={rect.Width},{rect.Height} BitmapWH={destinationBitmap.Width},{destinationBitmap.Height} D={destinationBitmap.RowBytes == (destinationBitmap.Width * sizeof(uint))}"); + + for (int row = rect.Top; row < rect.Bottom; row++) + { + if (row >= destinationBitmap.Height) + { + return false; + } + + var col = rect.Left; + uint* destinationPixelPtr = basePtr + destinationBitmap.Width * row + col; + uint* sourcePixelPtr = sourcePtr + sourceBitmap.Width * row + col; + + var length = rect.Width; + + if (col + length > destinationBitmap.Width) + { + return false; + } + + var byteCount = (uint) length * sizeof(uint); + Unsafe.CopyBlockUnaligned(destinationPixelPtr, sourcePixelPtr, byteCount); + } + + return true; + } + + /// + /// 清理指定范围 + /// + /// + /// + public static unsafe void ClearBounds(this SKBitmap bitmap, SKRectI rect) + { + // 等价于 Erase 方法 + //bitmap.Erase(SKColor.Empty, rect); + + uint* basePtr = (uint*) bitmap.GetPixels().ToPointer(); + // Loop through the rows + //var stopwatch = Stopwatch.StartNew(); + //for (int row = 0; row < bitmap.Height; row++) + //{ + // for (int col = 0; col < bitmap.Width; col++) + // { + // uint* ptr = basePtr + bitmap.Width * row + col; + // *ptr = unchecked((uint)(0xFF << 24 + ((byte)col) << + // 16 + (byte) row)); + // } + //} + + for (int row = rect.Top; row < rect.Bottom; row++) + { + var col = rect.Left; + uint* ptr = basePtr + bitmap.Width * row + col; + + var length = rect.Width; + Unsafe.InitBlock(ptr, 0, (uint) length * sizeof(uint)); + //var span = new Span(ptr, length); + //span.Clear(); + } + + //Console.WriteLine($"耗时 {stopwatch.ElapsedMilliseconds}"); // 差不多一秒 + } +} \ No newline at end of file