feat(core): SnapshotService — рендер 3D-модели в PNG (S4, "зрение агента")

- RasterImageFormat (PNG/JPG/BMP/...) + MIME-типы + парсинг
- SnapshotService: ksDocument3D.SaveAsToRasterFormat -> временный файл -> байты
  (resultArrayBytes при заданном имени файла не заполняется; рендер в файл надёжен)
- tests: снимок активной детали возвращает валидный PNG (визуально проверено)
- docs/OPEN_QUESTIONS.md: зафиксирован нюанс resultArrayBytes

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 23:48:35 +03:00
parent f0995a67e6
commit d4502409de
4 changed files with 181 additions and 0 deletions
@@ -0,0 +1,38 @@
namespace Kompas.Mcp.Core.Vision;
/// <summary>Формат растра для снимка модели (значения ksRasterFormatEnum / ksRasterFormatParam.format).</summary>
public enum RasterImageFormat : short
{
Bmp = 0,
Gif = 1,
Jpg = 2,
Png = 3,
Tif = 4,
Tga = 5,
}
public static class RasterImageFormats
{
/// <summary>MIME-тип для image-контента MCP.</summary>
public static string MimeType(this RasterImageFormat format) => format switch
{
RasterImageFormat.Png => "image/png",
RasterImageFormat.Jpg => "image/jpeg",
RasterImageFormat.Bmp => "image/bmp",
RasterImageFormat.Gif => "image/gif",
RasterImageFormat.Tif => "image/tiff",
RasterImageFormat.Tga => "image/x-tga",
_ => "application/octet-stream",
};
public static RasterImageFormat Parse(string value) => value.Trim().ToLowerInvariant() switch
{
"png" => RasterImageFormat.Png,
"jpg" or "jpeg" => RasterImageFormat.Jpg,
"bmp" => RasterImageFormat.Bmp,
"gif" => RasterImageFormat.Gif,
"tif" or "tiff" => RasterImageFormat.Tif,
"tga" => RasterImageFormat.Tga,
_ => throw new ArgumentException($"Неизвестный формат растра '{value}'.", nameof(value)),
};
}
@@ -0,0 +1,91 @@
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
namespace Kompas.Mcp.Core.Vision;
/// <summary>Результат снимка модели.</summary>
public sealed record SnapshotResult
{
public required byte[] Bytes { get; init; }
public required RasterImageFormat Format { get; init; }
public string MimeType => Format.MimeType();
}
/// <summary>
/// «Зрение агента»: рендер активного 3D-документа в растр (PNG и пр.) прямо в память
/// через API5 <c>ksDocument3D.SaveAsToRasterFormat</c> + <c>resultArrayBytes</c>.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class SnapshotService
{
private readonly KompasSession _session;
private readonly KompasDispatcher _dispatcher;
public SnapshotService(KompasSession session, KompasDispatcher dispatcher)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
/// <summary>
/// Снять текущий вид активного 3D-документа в растр.
/// </summary>
/// <param name="format">Формат изображения (по умолчанию PNG).</param>
/// <param name="resolution">Разрешение, DPI; 0 — текущее экранное.</param>
/// <param name="greyScale">Сохранять в оттенках серого.</param>
/// <param name="saveToFile">Если задан — дополнительно записать файл по пути.</param>
public Task<SnapshotResult> CaptureActiveModelAsync(
RasterImageFormat format = RasterImageFormat.Png,
int resolution = 0,
bool greyScale = false,
string? saveToFile = null,
CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => CaptureCore(format, resolution, greyScale, saveToFile), ct);
private SnapshotResult CaptureCore(RasterImageFormat format, int resolution, bool greyScale, string? saveToFile)
{
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
?? throw new InvalidOperationException("Нет активного 3D-документа для снимка.");
var param = doc3d.RasterFormatParam() as ksRasterFormatParam
?? throw new InvalidOperationException("Не удалось получить ksRasterFormatParam.");
param.Init();
param.format = (short)format;
param.colorBPP = 24;
param.extResolution = resolution;
param.greyScale = greyScale;
param.onlyThinLine = false;
// Рендерим в файл и читаем байты обратно: для 3D-документа resultArrayBytes
// при заданном имени файла не заполняется (см. docs/OPEN_QUESTIONS.md), а файловый
// вывод работает надёжно.
param.returnResultAsArrayBytes = false;
var outFile = saveToFile ?? Path.Combine(
Path.GetTempPath(), $"kompas-snap-{Guid.NewGuid():N}.{format.ToString().ToLowerInvariant()}");
var deleteAfter = saveToFile is null;
try
{
if (!doc3d.SaveAsToRasterFormat(outFile, param))
throw new InvalidOperationException("SaveAsToRasterFormat вернул FALSE (рендер не удался).");
if (!File.Exists(outFile))
throw new InvalidOperationException("Файл снимка не создан.");
var bytes = File.ReadAllBytes(outFile);
if (bytes.Length == 0)
throw new InvalidOperationException("Снимок пуст.");
return new SnapshotResult { Bytes = bytes, Format = format };
}
finally
{
if (deleteAfter && File.Exists(outFile))
{
try { File.Delete(outFile); } catch { /* временный файл — не критично */ }
}
}
}
}