diff --git a/docs/OPEN_QUESTIONS.md b/docs/OPEN_QUESTIONS.md
index a004845..9d13625 100644
--- a/docs/OPEN_QUESTIONS.md
+++ b/docs/OPEN_QUESTIONS.md
@@ -31,3 +31,10 @@
## Журнал решений по ходу
(дополняется автоматически во время работы)
+
+### ✅ Снимок 3D: рендер через файл, а не resultArrayBytes
+`ksDocument3D.SaveAsToRasterFormat(fileName, param)` при **непустом** `fileName` возвращает TRUE,
+пишет корректный PNG на диск, но `param.resultArrayBytes` остаётся **пустым**. Поэтому
+`SnapshotService` рендерит во временный файл и читает байты обратно (надёжно проверено на v24 Home).
+**На проработку:** пустое имя файла + `returnResultAsArrayBytes=true` для чистого in-memory —
+не проверено; текущий путь через temp-файл работает.
diff --git a/src/Kompas.Mcp.Core/Vision/RasterImageFormat.cs b/src/Kompas.Mcp.Core/Vision/RasterImageFormat.cs
new file mode 100644
index 0000000..27357fc
--- /dev/null
+++ b/src/Kompas.Mcp.Core/Vision/RasterImageFormat.cs
@@ -0,0 +1,38 @@
+namespace Kompas.Mcp.Core.Vision;
+
+/// Формат растра для снимка модели (значения ksRasterFormatEnum / ksRasterFormatParam.format).
+public enum RasterImageFormat : short
+{
+ Bmp = 0,
+ Gif = 1,
+ Jpg = 2,
+ Png = 3,
+ Tif = 4,
+ Tga = 5,
+}
+
+public static class RasterImageFormats
+{
+ /// MIME-тип для image-контента MCP.
+ 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)),
+ };
+}
diff --git a/src/Kompas.Mcp.Core/Vision/SnapshotService.cs b/src/Kompas.Mcp.Core/Vision/SnapshotService.cs
new file mode 100644
index 0000000..d839ca7
--- /dev/null
+++ b/src/Kompas.Mcp.Core/Vision/SnapshotService.cs
@@ -0,0 +1,91 @@
+using System.Runtime.Versioning;
+using Kompas.Mcp.Core.Threading;
+using Kompas6API5;
+
+namespace Kompas.Mcp.Core.Vision;
+
+/// Результат снимка модели.
+public sealed record SnapshotResult
+{
+ public required byte[] Bytes { get; init; }
+ public required RasterImageFormat Format { get; init; }
+ public string MimeType => Format.MimeType();
+}
+
+///
+/// «Зрение агента»: рендер активного 3D-документа в растр (PNG и пр.) прямо в память
+/// через API5 ksDocument3D.SaveAsToRasterFormat + resultArrayBytes.
+///
+[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));
+ }
+
+ ///
+ /// Снять текущий вид активного 3D-документа в растр.
+ ///
+ /// Формат изображения (по умолчанию PNG).
+ /// Разрешение, DPI; 0 — текущее экранное.
+ /// Сохранять в оттенках серого.
+ /// Если задан — дополнительно записать файл по пути.
+ public Task 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 { /* временный файл — не критично */ }
+ }
+ }
+ }
+}
diff --git a/tests/Kompas.Mcp.Tests/Integration/SnapshotTests.cs b/tests/Kompas.Mcp.Tests/Integration/SnapshotTests.cs
new file mode 100644
index 0000000..1b8d00a
--- /dev/null
+++ b/tests/Kompas.Mcp.Tests/Integration/SnapshotTests.cs
@@ -0,0 +1,45 @@
+using Kompas.Mcp.Core.Documents;
+using Kompas.Mcp.Core.Vision;
+
+namespace Kompas.Mcp.Tests.Integration;
+
+/// Интеграция: снимок модели в растр («зрение агента»).
+[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);
+ }
+ }
+}