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
-
+