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:
@@ -31,3 +31,10 @@
|
||||
|
||||
## Журнал решений по ходу
|
||||
(дополняется автоматически во время работы)
|
||||
|
||||
### ✅ Снимок 3D: рендер через файл, а не resultArrayBytes
|
||||
`ksDocument3D.SaveAsToRasterFormat(fileName, param)` при **непустом** `fileName` возвращает TRUE,
|
||||
пишет корректный PNG на диск, но `param.resultArrayBytes` остаётся **пустым**. Поэтому
|
||||
`SnapshotService` рендерит во временный файл и читает байты обратно (надёжно проверено на v24 Home).
|
||||
**На проработку:** пустое имя файла + `returnResultAsArrayBytes=true` для чистого in-memory —
|
||||
не проверено; текущий путь через temp-файл работает.
|
||||
|
||||
@@ -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 { /* временный файл — не критично */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Vision;
|
||||
|
||||
namespace Kompas.Mcp.Tests.Integration;
|
||||
|
||||
/// <summary>Интеграция: снимок модели в растр («зрение агента»).</summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Collection(KompasCollection.Name)]
|
||||
public sealed class SnapshotTests
|
||||
{
|
||||
private readonly DocumentService _docs;
|
||||
private readonly SnapshotService _snap;
|
||||
|
||||
public SnapshotTests(KompasFixture fx)
|
||||
{
|
||||
_docs = new DocumentService(fx.Session, fx.Dispatcher);
|
||||
_snap = new SnapshotService(fx.Session, fx.Dispatcher);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Snapshot_active_part_returns_valid_png()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
var path = TestPaths.NewFile(".png");
|
||||
var snap = await _snap.CaptureActiveModelAsync(RasterImageFormat.Png, saveToFile: path);
|
||||
|
||||
Assert.Equal(RasterImageFormat.Png, snap.Format);
|
||||
Assert.True(snap.Bytes.Length > 100, "Снимок подозрительно мал.");
|
||||
|
||||
// Сигнатура PNG: 89 50 4E 47 0D 0A 1A 0A
|
||||
Assert.Equal(0x89, snap.Bytes[0]);
|
||||
Assert.Equal((byte)'P', snap.Bytes[1]);
|
||||
Assert.Equal((byte)'N', snap.Bytes[2]);
|
||||
Assert.Equal((byte)'G', snap.Bytes[3]);
|
||||
|
||||
Assert.True(File.Exists(path));
|
||||
}
|
||||
finally
|
||||
{
|
||||
await _docs.CloseAsync(save: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user