From a61bb43250768e26181d34d341ed2d573d2f7eec Mon Sep 17 00:00:00 2001 From: ww-rm Date: Sun, 26 Oct 2025 22:14:34 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0preview=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SpineViewerCLI/PreviewCommand.cs | 133 +++++++++++++++++++++++++++ SpineViewerCLI/SpineViewerCLI.cs | 11 +-- SpineViewerCLI/SpineViewerCLI.csproj | 2 + 3 files changed, 139 insertions(+), 7 deletions(-) create mode 100644 SpineViewerCLI/PreviewCommand.cs diff --git a/SpineViewerCLI/PreviewCommand.cs b/SpineViewerCLI/PreviewCommand.cs new file mode 100644 index 0000000..8b0fa56 --- /dev/null +++ b/SpineViewerCLI/PreviewCommand.cs @@ -0,0 +1,133 @@ +using NLog; +using Spectre.Console; +using Spine; +using Spine.Exporters; +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SpineViewerCLI +{ + public class PreviewCommand : Command + { + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + private static readonly string _name = "preview"; + private static readonly string _desc = "Preview a model"; + private static readonly int MaxResolution = 1024; + + public Argument ArgSkel { get; } = new("skel") + { + Description = "Path of skel file.", + }; + + public Option OptAtlas { get; } = new("--atlas") + { + Description = "Path to the atlas file that matches the skel file.", + }; + + public Option OptPma { get; } = new("--pma") + { + Description = "Specifies whether the texture uses PMA (premultiplied alpha) format.", + }; + + public Option OptSkins { get; } = new("--skins") + { + Description = "Skins to enable. Multiple skins can be specified.", + Arity = ArgumentArity.OneOrMore, + AllowMultipleArgumentsPerToken = true, + }; + + public Option OptAnimations { get; } = new("--animations") + { + Description = "Animations to export. Supports multiple entries, placed in order on tracks starting from 0.", + Arity = ArgumentArity.OneOrMore, + AllowMultipleArgumentsPerToken = true, + }; + + public Option OptTime { get; } = new("--time") + { + Description = "Start time offset of the animation.", + DefaultValueFactory = _ => 0f, + }; + + public PreviewCommand() : base(_name, _desc) + { + OptTime.Validators.Add(r => + { + if (r.Tokens.Count > 0 && float.TryParse(r.Tokens[0].Value, out var v) && v < 0) + r.AddError($"{OptTime.Name} must be non-negative."); + }); + + this.AddArgsAndOpts(); + SetAction(PreviewAction); + } + + private void PreviewAction(ParseResult result) + { + // 读取模型 + using var spine = new SpineObject(result.GetValue(ArgSkel)!.FullName, result.GetValue(OptAtlas)?.FullName); + + spine.UsePma = result.GetValue(OptPma); + + // 设置要导出的动画 + int trackIdx = 0; + foreach (var name in result.GetValue(OptAnimations)) + { + if (!spine.Data.AnimationsByName.ContainsKey(name)) + { + _logger.Warn("No animation named '{0}', skip it", name); + continue; + } + spine.AnimationState.SetAnimation(trackIdx, name, true); + trackIdx++; + } + + // 设置需要启用的皮肤 + foreach (var name in result.GetValue(OptSkins)) + { + if (!spine.SetSkinStatus(name, true)) + { + _logger.Warn("Failed to enable skin '{0}'", name); + } + } + + // 设置时间偏移量 + spine.Update(result.GetValue(OptTime)); + + using var exporter = GetExporterFilledWithArgs(result, spine); + using var skImage = exporter.ExportMemoryImage(spine); + using var skData = skImage.Encode(); + var img = new CanvasImage(skData.AsSpan()); + AnsiConsole.Write(img); + } + + private FrameExporter GetExporterFilledWithArgs(ParseResult result, SpineObject spine) + { + // 根据模型获取自动分辨率和视区参数 + var bounds = spine.GetCurrentBounds(); + var resolution = new SFML.System.Vector2u((uint)bounds.Size.X, (uint)bounds.Size.Y); + if (resolution.X >= MaxResolution || resolution.Y >= MaxResolution) + { + // 缩小到最大像素限制 + var scale = Math.Min(MaxResolution / bounds.Width, MaxResolution / bounds.Height); + resolution.X = (uint)(bounds.Width * scale); + resolution.Y = (uint)(bounds.Height * scale); + } + var viewBounds = bounds.GetCanvasBounds(resolution); + + return new FrameExporter(resolution) + { + Size = new(viewBounds.Width, -viewBounds.Height), + Center = viewBounds.Position + viewBounds.Size / 2, + Rotation = 0, + BackgroundColor = SFML.Graphics.Color.Transparent, + + Format = SkiaSharp.SKEncodedImageFormat.Png, + Quality = 100, + }; + } + } +} diff --git a/SpineViewerCLI/SpineViewerCLI.cs b/SpineViewerCLI/SpineViewerCLI.cs index b8fd01a..8615ad9 100644 --- a/SpineViewerCLI/SpineViewerCLI.cs +++ b/SpineViewerCLI/SpineViewerCLI.cs @@ -1,7 +1,6 @@ using NLog; -using SFML.Graphics; -using SFML.System; using SkiaSharp; +using Spectre.Console; using Spine; using Spine.Exporters; using System.CommandLine; @@ -21,14 +20,12 @@ namespace SpineViewerCLI { InitializeFileLog(); - var cmdQuery = new QueryCommand(); - var cmdExport = new ExportCommand(); - var cmdRoot = new RootCommand("Root Command") { OptQuiet, - cmdQuery, - cmdExport, + new QueryCommand(), + new PreviewCommand(), + new ExportCommand(), }; var result = cmdRoot.Parse(args); diff --git a/SpineViewerCLI/SpineViewerCLI.csproj b/SpineViewerCLI/SpineViewerCLI.csproj index d34e33f..a40d269 100644 --- a/SpineViewerCLI/SpineViewerCLI.csproj +++ b/SpineViewerCLI/SpineViewerCLI.csproj @@ -17,6 +17,8 @@ + +