From 0cf6833876e14d11aabcee79724525de6d478b99 Mon Sep 17 00:00:00 2001 From: lincube Date: Sat, 11 Jul 2026 02:27:18 +0900 Subject: [PATCH] feat: introduce AirApp SDK, InkCanvas components, and background service infrastructure with updated dependencies --- Directory.Packages.props | 56 +++++---- .../LanDesktopPLONDS.installer.csproj | 2 + .../LanMountainDesktop.AirAppDevServer.csproj | 8 +- LanMountainDesktop.AirAppDevServer/Program.cs | 107 ++++++++++-------- .../AirAppWindowBase.cs | 9 +- .../LanMountainDesktop.AirAppSdk.csproj | 7 +- .../LanMountainDesktop.Launcher.csproj | 1 + .../Shell/LauncherBackgroundService.cs | 97 +++++++++++++--- .../DesktopEditOverlayPresenterTests.cs | 9 +- .../LanMountainDesktop.Tests.csproj | 3 +- .../LauncherBackgroundServiceTests.cs | 38 +------ LanMountainDesktop/LanMountainDesktop.csproj | 1 + .../LinuxMprisMusicSessionProvider.cs | 2 +- NuGet.Config | 4 + ...Campus.AvaloniaInkCanvas.Avalonia12.csproj | 8 +- .../Caching/InkBitmapCache.cs | 4 +- .../DotNetCampus.AvaloniaInkCanvas.csproj | 5 +- .../DotNetCampus.InkCanvas.InkCore.csproj | 4 +- .../Inking/Primitive/RotatedRect.cs | 20 +++- .../DotNetCampus.InkCanvas.SkiaInk.csproj | 2 +- 20 files changed, 241 insertions(+), 146 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e566f35..f74d74b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,44 +3,54 @@ true - - + + - - - - + + + + + + - - - - + + + + + + + - - - - - + + + + + + + + - - - - + + + + + + - - + + - - + + diff --git a/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj b/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj index 097b46c..75daeef 100644 --- a/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj +++ b/LanDesktopPLONDS.installer/LanDesktopPLONDS.installer.csproj @@ -23,7 +23,9 @@ + + diff --git a/LanMountainDesktop.AirAppDevServer/LanMountainDesktop.AirAppDevServer.csproj b/LanMountainDesktop.AirAppDevServer/LanMountainDesktop.AirAppDevServer.csproj index 768461a..f010838 100644 --- a/LanMountainDesktop.AirAppDevServer/LanMountainDesktop.AirAppDevServer.csproj +++ b/LanMountainDesktop.AirAppDevServer/LanMountainDesktop.AirAppDevServer.csproj @@ -8,10 +8,10 @@ - - - - + + + + diff --git a/LanMountainDesktop.AirAppDevServer/Program.cs b/LanMountainDesktop.AirAppDevServer/Program.cs index c301e0d..b6edbfa 100644 --- a/LanMountainDesktop.AirAppDevServer/Program.cs +++ b/LanMountainDesktop.AirAppDevServer/Program.cs @@ -15,64 +15,77 @@ class Program // 开发模式命令 var devCommand = new Command("dev", "启动开发服务器(支持热重载)"); - var projectPathOption = new Option( - aliases: new[] { "--project", "-p" }, - description: "AirApp 项目路径", - getDefaultValue: () => Directory.GetCurrentDirectory()); - var portOption = new Option( - aliases: new[] { "--port" }, - description: "开发服务器端口", - getDefaultValue: () => 5000); - var verboseOption = new Option( - aliases: new[] { "--verbose", "-v" }, - description: "显示详细日志"); - - devCommand.AddOption(projectPathOption); - devCommand.AddOption(portOption); - devCommand.AddOption(verboseOption); - - devCommand.SetHandler(async (projectPath, port, verbose) => + var projectPathOption = new Option("--project", "-p") { - await RunDevServerAsync(projectPath, port, verbose); - }, projectPathOption, portOption, verboseOption); + Description = "AirApp 项目路径", + DefaultValueFactory = _ => Directory.GetCurrentDirectory(), + Recursive = true + }; + var portOption = new Option("--port") + { + Description = "开发服务器端口", + DefaultValueFactory = _ => 5000 + }; + var verboseOption = new Option("--verbose", "-v") + { + Description = "显示详细日志" + }; + + rootCommand.Options.Add(projectPathOption); + devCommand.Options.Add(portOption); + devCommand.Options.Add(verboseOption); + + devCommand.SetAction(async parseResult => + { + await RunDevServerAsync( + parseResult.GetValue(projectPathOption) ?? Directory.GetCurrentDirectory(), + parseResult.GetValue(portOption), + parseResult.GetValue(verboseOption)); + }); // 预览命令 var previewCommand = new Command("preview", "预览 AirApp(无需安装到宿主)"); - var componentOption = new Option( - aliases: new[] { "--component", "-c" }, - description: "要预览的组件 ID"); - var windowOption = new Option( - aliases: new[] { "--window", "-w" }, - description: "要预览的窗口 ID"); - - previewCommand.AddOption(projectPathOption); - previewCommand.AddOption(componentOption); - previewCommand.AddOption(windowOption); - - previewCommand.SetHandler(async (projectPath, component, window) => + var componentOption = new Option("--component", "-c") { - await RunPreviewAsync(projectPath, component, window); - }, projectPathOption, componentOption, windowOption); + Description = "要预览的组件 ID" + }; + var windowOption = new Option("--window", "-w") + { + Description = "要预览的窗口 ID" + }; + + previewCommand.Options.Add(componentOption); + previewCommand.Options.Add(windowOption); + + previewCommand.SetAction(async parseResult => + { + await RunPreviewAsync( + parseResult.GetValue(projectPathOption) ?? Directory.GetCurrentDirectory(), + parseResult.GetValue(componentOption), + parseResult.GetValue(windowOption)); + }); // 打包命令 var packageCommand = new Command("package", "打包 AirApp 为 .laapp 文件"); - var outputOption = new Option( - aliases: new[] { "--output", "-o" }, - description: "输出路径"); - - packageCommand.AddOption(projectPathOption); - packageCommand.AddOption(outputOption); - - packageCommand.SetHandler(async (projectPath, output) => + var outputOption = new Option("--output", "-o") { - await PackageAirAppAsync(projectPath, output); - }, projectPathOption, outputOption); + Description = "输出路径" + }; - rootCommand.AddCommand(devCommand); - rootCommand.AddCommand(previewCommand); - rootCommand.AddCommand(packageCommand); + packageCommand.Options.Add(outputOption); - return await rootCommand.InvokeAsync(args); + packageCommand.SetAction(async parseResult => + { + await PackageAirAppAsync( + parseResult.GetValue(projectPathOption) ?? Directory.GetCurrentDirectory(), + parseResult.GetValue(outputOption)); + }); + + rootCommand.Subcommands.Add(devCommand); + rootCommand.Subcommands.Add(previewCommand); + rootCommand.Subcommands.Add(packageCommand); + + return await rootCommand.Parse(args).InvokeAsync(); } static async Task RunDevServerAsync(string projectPath, int port, bool verbose) diff --git a/LanMountainDesktop.AirAppSdk/AirAppWindowBase.cs b/LanMountainDesktop.AirAppSdk/AirAppWindowBase.cs index a5ec528..154c69c 100644 --- a/LanMountainDesktop.AirAppSdk/AirAppWindowBase.cs +++ b/LanMountainDesktop.AirAppSdk/AirAppWindowBase.cs @@ -72,23 +72,22 @@ public abstract class AirAppWindowBase : Window, IAirAppWindow MinHeight = descriptor.MinHeight; CanResize = descriptor.CanResize; ShowInTaskbar = descriptor.ShowInTaskbar; - ShowAsDialog = descriptor.ShowAsDialog; // Apply chrome mode switch (descriptor.ChromeMode) { case AirAppWindowChromeMode.Standard: - SystemDecorations = SystemDecorations.Full; + WindowDecorations = Avalonia.Controls.WindowDecorations.Full; break; case AirAppWindowChromeMode.Borderless: - SystemDecorations = SystemDecorations.BorderOnly; + WindowDecorations = Avalonia.Controls.WindowDecorations.BorderOnly; break; case AirAppWindowChromeMode.FullScreen: - SystemDecorations = SystemDecorations.None; + WindowDecorations = Avalonia.Controls.WindowDecorations.None; WindowState = WindowState.FullScreen; break; case AirAppWindowChromeMode.Tool: - SystemDecorations = SystemDecorations.Full; + WindowDecorations = Avalonia.Controls.WindowDecorations.Full; ShowInTaskbar = false; break; } diff --git a/LanMountainDesktop.AirAppSdk/LanMountainDesktop.AirAppSdk.csproj b/LanMountainDesktop.AirAppSdk/LanMountainDesktop.AirAppSdk.csproj index 0476e32..aec2265 100644 --- a/LanMountainDesktop.AirAppSdk/LanMountainDesktop.AirAppSdk.csproj +++ b/LanMountainDesktop.AirAppSdk/LanMountainDesktop.AirAppSdk.csproj @@ -20,10 +20,9 @@ - - - - + + + diff --git a/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj b/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj index e0ea68b..55abac2 100644 --- a/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj +++ b/LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj @@ -31,6 +31,7 @@ + diff --git a/LanMountainDesktop.Launcher/Shell/LauncherBackgroundService.cs b/LanMountainDesktop.Launcher/Shell/LauncherBackgroundService.cs index 41acae7..e3994a0 100644 --- a/LanMountainDesktop.Launcher/Shell/LauncherBackgroundService.cs +++ b/LanMountainDesktop.Launcher/Shell/LauncherBackgroundService.cs @@ -1,4 +1,5 @@ using Avalonia.Media.Imaging; +using SkiaSharp; namespace LanMountainDesktop.Launcher.Shell; @@ -21,6 +22,8 @@ internal static class LauncherBackgroundService private static string? _cachedPath; private static long _cachedLength; private static DateTime _cachedLastWriteTimeUtc; + private static int _cachedWidth; + private static int _cachedHeight; internal static string? LauncherDataDirectoryOverride { get; set; } @@ -79,18 +82,37 @@ internal static class LauncherBackgroundService IsValid = true, FilePath = imagePath, Bitmap = _cachedBitmap, - Width = _cachedBitmap!.PixelSize.Width, - Height = _cachedBitmap.PixelSize.Height, - AspectRatio = (double)_cachedBitmap.PixelSize.Width / _cachedBitmap.PixelSize.Height + Width = _cachedWidth, + Height = _cachedHeight, + AspectRatio = (double)_cachedWidth / _cachedHeight }; } DisposeCache(); + if (!TryInspectImage(imagePath, out var decodedWidth, out var decodedHeight, out var decodeError)) + { + return new BackgroundImageInfo + { + Exists = true, + IsValid = false, + FilePath = imagePath, + ErrorMessage = $"Image could not be decoded: {decodeError}" + }; + } + Bitmap bitmap; try { - bitmap = new Bitmap(imagePath); + // Decode from a stream instead of the path-based constructor. Avalonia/Skia may + // retain a path-backed image source, which can return stale pixels when a file is + // replaced in place. + using var imageStream = new FileStream( + imagePath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite | FileShare.Delete); + bitmap = new Bitmap(imageStream); } catch (Exception ex) { @@ -103,14 +125,16 @@ internal static class LauncherBackgroundService }; } - var width = bitmap.PixelSize.Width; - var height = bitmap.PixelSize.Height; + var width = decodedWidth; + var height = decodedHeight; var aspectRatio = height == 0 ? 0d : (double)width / height; _cachedBitmap = bitmap; _cachedPath = imagePath; _cachedLength = fileInfo.Length; _cachedLastWriteTimeUtc = fileInfo.LastWriteTimeUtc; + _cachedWidth = width; + _cachedHeight = height; Logger.Info($"[LauncherBackground] Background image loaded: {imagePath} ({width}x{height})."); @@ -164,14 +188,9 @@ internal static class LauncherBackgroundService return FailMutation($"Image file is too large ({sourceInfo.Length / 1024 / 1024}MB > 10MB)."); } - try + if (!TryInspectImage(fullSourcePath, out _, out _, out var decodeError)) { - using var bitmap = new Bitmap(fullSourcePath); - _ = bitmap.PixelSize; - } - catch (Exception ex) - { - return FailMutation($"The selected image could not be decoded: {ex.Message}"); + return FailMutation($"The selected image could not be decoded: {decodeError}"); } var launcherPath = ResolveLauncherDataPath(); @@ -235,6 +254,8 @@ internal static class LauncherBackgroundService _cachedPath = null; _cachedLength = 0; _cachedLastWriteTimeUtc = DateTime.MinValue; + _cachedWidth = 0; + _cachedHeight = 0; } internal static string? FindManagedImageFile() @@ -261,6 +282,56 @@ internal static class LauncherBackgroundService _cachedLastWriteTimeUtc == fileInfo.LastWriteTimeUtc; } + private static bool TryInspectImage( + string path, + out int width, + out int height, + out string? error) + { + width = 0; + height = 0; + error = null; + + try + { + using var stream = new FileStream( + path, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite | FileShare.Delete); + using var codec = SKCodec.Create(stream); + if (codec is null) + { + error = "The file is not a recognized image."; + return false; + } + + var info = codec.Info; + if (info.Width <= 0 || info.Height <= 0) + { + error = "The image has invalid dimensions."; + return false; + } + + using var pixels = new SKBitmap(info); + var result = codec.GetPixels(info, pixels.GetPixels()); + if (result is not (SKCodecResult.Success or SKCodecResult.IncompleteInput)) + { + error = $"The decoder returned {result}."; + return false; + } + + width = info.Width; + height = info.Height; + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + } + private static string? FindImageFile(string directory) { if (!Directory.Exists(directory)) diff --git a/LanMountainDesktop.Tests/DesktopEditOverlayPresenterTests.cs b/LanMountainDesktop.Tests/DesktopEditOverlayPresenterTests.cs index 252a961..7f62526 100644 --- a/LanMountainDesktop.Tests/DesktopEditOverlayPresenterTests.cs +++ b/LanMountainDesktop.Tests/DesktopEditOverlayPresenterTests.cs @@ -1,6 +1,7 @@ using System.Linq; using Avalonia; using Avalonia.Controls; +using Avalonia.Headless.XUnit; using LanMountainDesktop.DesktopEditing; using Xunit; @@ -8,7 +9,7 @@ namespace LanMountainDesktop.Tests; public sealed class DesktopEditOverlayPresenterTests { - [Fact] + [AvaloniaFact] public void CompositionOffsetHelperFallsBackWhenVisualIsUnavailable() { var service = new CompositionVisualAnimationService(_ => null); @@ -21,7 +22,7 @@ public sealed class DesktopEditOverlayPresenterTests Assert.False(service.TrySetUniformScale(target, 1.05)); } - [Fact] + [AvaloniaFact] public void PreviewRectUsesCanvasPlacementWhenCompositionIsUnavailable() { var presenter = new DesktopEditOverlayPresenter(new CompositionVisualAnimationService(_ => null)); @@ -36,7 +37,7 @@ public sealed class DesktopEditOverlayPresenterTests Assert.Equal(120, ghost.Height); } - [Fact] + [AvaloniaFact] public void CandidateRectUsesCanvasPlacement() { var presenter = new DesktopEditOverlayPresenter(new CompositionVisualAnimationService(_ => null)); @@ -51,7 +52,7 @@ public sealed class DesktopEditOverlayPresenterTests Assert.Equal(160, candidate.Height); } - [Fact] + [AvaloniaFact] public void ShowPreservesPreviewAndCandidateCanvasPlacement() { var presenter = new DesktopEditOverlayPresenter(new CompositionVisualAnimationService(_ => null)); diff --git a/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj b/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj index 67a5135..763edb9 100644 --- a/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj +++ b/LanMountainDesktop.Tests/LanMountainDesktop.Tests.csproj @@ -8,8 +8,9 @@ + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/LanMountainDesktop.Tests/LauncherBackgroundServiceTests.cs b/LanMountainDesktop.Tests/LauncherBackgroundServiceTests.cs index 90f721a..1a2ed81 100644 --- a/LanMountainDesktop.Tests/LauncherBackgroundServiceTests.cs +++ b/LanMountainDesktop.Tests/LauncherBackgroundServiceTests.cs @@ -1,4 +1,4 @@ -using Avalonia; +using Avalonia.Headless.XUnit; using LanMountainDesktop.Launcher.Shell; using Xunit; @@ -17,13 +17,8 @@ public sealed class LauncherBackgroundServiceTests : IDisposable private readonly string _tempDirectory; private readonly string _launcherDataDirectory; - private static readonly object AvaloniaGate = new(); - private static bool _avaloniaInitialized; - public LauncherBackgroundServiceTests() { - EnsureAvaloniaInitialized(); - _tempDirectory = Path.Combine( Path.GetTempPath(), "LanMountainDesktop.BackgroundImageTests", @@ -34,28 +29,7 @@ public sealed class LauncherBackgroundServiceTests : IDisposable LauncherBackgroundService.ClearCache(); } - private static void EnsureAvaloniaInitialized() - { - lock (AvaloniaGate) - { - if (_avaloniaInitialized) - { - return; - } - - if (Application.Current is null) - { - AppBuilder - .Configure() - .UsePlatformDetect() - .SetupWithoutStarting(); - } - - _avaloniaInitialized = true; - } - } - - [Fact] + [AvaloniaFact] public void SaveBackgroundImage_CopiesSelectedImageToLauncherDataDirectory() { var sourcePath = WriteImage("selected.png", RedPng1x1); @@ -68,7 +42,7 @@ public sealed class LauncherBackgroundServiceTests : IDisposable Assert.Equal(File.ReadAllBytes(sourcePath), File.ReadAllBytes(result.FilePath)); } - [Fact] + [AvaloniaFact] public void SaveBackgroundImage_ReplacesPreviousManagedExtension() { var pngSourcePath = WriteImage("first.png", RedPng1x1); @@ -83,7 +57,7 @@ public sealed class LauncherBackgroundServiceTests : IDisposable Assert.True(File.Exists(Path.Combine(_launcherDataDirectory, "Launcher Picture.jpg"))); } - [Fact] + [AvaloniaFact] public void LoadBackgroundImage_AcceptsNonSevenByFiveImage() { var sourcePath = WriteImage("square.png", RedPng1x1); @@ -97,7 +71,7 @@ public sealed class LauncherBackgroundServiceTests : IDisposable Assert.Equal(1, imageInfo.Height); } - [Theory] + [AvaloniaTheory] [InlineData("oversized.png", InvalidImageKind.Oversized)] [InlineData("unknown.txt", InvalidImageKind.UnknownExtension)] [InlineData("broken.png", InvalidImageKind.BrokenImage)] @@ -118,7 +92,7 @@ public sealed class LauncherBackgroundServiceTests : IDisposable Assert.Equal(originalBytes, File.ReadAllBytes(managedPath)); } - [Fact] + [AvaloniaFact] public void LoadBackgroundImage_WhenFileChangesAtSamePath_RefreshesCachedBitmap() { var sourcePath = WriteImage("source.png", RedPng1x1); diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj index 6537277..40d88de 100644 --- a/LanMountainDesktop/LanMountainDesktop.csproj +++ b/LanMountainDesktop/LanMountainDesktop.csproj @@ -73,6 +73,7 @@ + diff --git a/LanMountainDesktop/Services/LinuxMprisMusicSessionProvider.cs b/LanMountainDesktop/Services/LinuxMprisMusicSessionProvider.cs index 6096c2d..527285d 100644 --- a/LanMountainDesktop/Services/LinuxMprisMusicSessionProvider.cs +++ b/LanMountainDesktop/Services/LinuxMprisMusicSessionProvider.cs @@ -229,7 +229,7 @@ internal sealed class LinuxMprisMusicSessionProvider : IMusicSessionProvider "NameOwnerChanged", ex => { - if (ex is null || !ActionException.IsObserverDisposed(ex)) + if (ex is null || !ObserverHandler.IsObserverDisposed(ex)) { SessionsChanged?.Invoke(this, EventArgs.Empty); } diff --git a/NuGet.Config b/NuGet.Config index 696b4d2..44e05b9 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,5 +1,9 @@ + + + + diff --git a/ThirdParty/DotNetCampus.InkCanvas/DotNetCampus.AvaloniaInkCanvas.Avalonia12.csproj b/ThirdParty/DotNetCampus.InkCanvas/DotNetCampus.AvaloniaInkCanvas.Avalonia12.csproj index 5a2cca4..1afe83c 100644 --- a/ThirdParty/DotNetCampus.InkCanvas/DotNetCampus.AvaloniaInkCanvas.Avalonia12.csproj +++ b/ThirdParty/DotNetCampus.InkCanvas/DotNetCampus.AvaloniaInkCanvas.Avalonia12.csproj @@ -7,12 +7,14 @@ DotNetCampus.AvaloniaInkCanvas DotNetCampus.Inking false + $(DefaultItemExcludes);src/**/bin/**;src/**/obj/** - - - + + + + diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Caching/InkBitmapCache.cs b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Caching/InkBitmapCache.cs index 86db73f..d0a3bc7 100644 --- a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Caching/InkBitmapCache.cs +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/Caching/InkBitmapCache.cs @@ -281,8 +281,8 @@ internal sealed class InkBitmapCache : IDisposable { using var paint = new SKPaint(); paint.IsAntialias = true; - paint.FilterQuality = SKFilterQuality.High; - canvas.DrawBitmap(data.Bitmap, 0, 0, paint); + var sampling = new SKSamplingOptions(SKCubicResampler.Mitchell); + canvas.DrawBitmap(data.Bitmap, 0, 0, sampling, paint); } finally { diff --git a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/DotNetCampus.AvaloniaInkCanvas.csproj b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/DotNetCampus.AvaloniaInkCanvas.csproj index 0617961..0755881 100644 --- a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/DotNetCampus.AvaloniaInkCanvas.csproj +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.AvaloniaInkCanvas/DotNetCampus.AvaloniaInkCanvas.csproj @@ -8,9 +8,8 @@ true - - - + + 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 index e31787a..bc5d49d 100644 --- a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/DotNetCampus.InkCanvas.InkCore.csproj +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/DotNetCampus.InkCanvas.InkCore.csproj @@ -9,8 +9,8 @@ - - + + 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 index aceceab..bf2d7ac 100644 --- a/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Primitive/RotatedRect.cs +++ b/ThirdParty/DotNetCampus.InkCanvas/src/DotNetCampus.InkCanvas.InkCore/Inking/Primitive/RotatedRect.cs @@ -56,6 +56,24 @@ public readonly record struct RotatedRect : ISimilarityTransformable2D + public RotatedRect ScaleTransform(double scale) + { + return Transform(SimilarityTransformation2D.Identity.Scale(scale)); + } + + /// + public RotatedRect RotateTransform(AngularMeasure rotation) + { + return Transform(SimilarityTransformation2D.Identity.Rotate(rotation)); + } + + /// + public RotatedRect TranslateTransform(Vector2D translation) + { + return Transform(SimilarityTransformation2D.Identity.Translate(translation)); + } + /// /// 判断指定的点是否在矩形内。这里考虑了矩形的旋转。 /// @@ -290,4 +308,4 @@ public readonly record struct RotatedRect : ISimilarityTransformable2D - +