diff --git a/docs/superpowers/specs/2026-05-27-drawing-standard-views-design.md b/docs/superpowers/specs/2026-05-27-drawing-standard-views-design.md
index 8f29851..78a275e 100644
--- a/docs/superpowers/specs/2026-05-27-drawing-standard-views-design.md
+++ b/docs/superpowers/specs/2026-05-27-drawing-standard-views-design.md
@@ -1,7 +1,22 @@
# Дизайн: стандартные виды чертежа (drawing_create_standard_views) через API7
**Дата:** 2026-05-27
-**Статус:** спайк зелёный (механизм подтверждён вживую); спроектировано; на ревью (Codex)
+**Статус:** спайк зелёный; ревью Codex учтено (10 замечаний); реализуется
+
+## Решения по ревью Codex
+
+- **#1** файл модели: `File.Exists` И `FileInfo.Length > 0` (несохранённый/битый файл).
+- **#3/#5/#6** непустота видов — гарантия В СЕРВИСЕ: после создания суммируем `IView.ObjectCount`
+ по коллекции; если 0 → ошибка «виды пусты (модель не спроецировалась)». Это и проверяет, что
+ rebuild не нужен (геометрия уже есть). Координатные проверки (`GetProjectionPoint`) — избыточны.
+- **#8** строгий контракт: `created == 3` (иначе ошибка с `before/after/ok`), а не `> 0`.
+- **#9** позиция главного вида — опциональные `x`,`y` (дефолт 100/150), чтобы повторный вызов не
+ клал виды стопкой; `dx`/`dy`=20 фиксированы (YAGNI).
+- **#10** возврат — record `DrawingViewsResult { Created, Total }` (не голый int) для отчёта и
+ будущей адресации видов.
+- **#2** негативный unit-тест на чужое расширение (`.step`) — в плане.
+- **#4** точечный release RCW отклонён (конвенция v2-2, как `AssemblyService`). **#7** неверный
+ `ProjectionName` не тестируем — `"#Спереди"` захардкожен, не управляется пользователем.
## Цель
diff --git a/src/Kompas.Mcp.Core/Drawings/DrawingService.cs b/src/Kompas.Mcp.Core/Drawings/DrawingService.cs
new file mode 100644
index 0000000..3b290c0
--- /dev/null
+++ b/src/Kompas.Mcp.Core/Drawings/DrawingService.cs
@@ -0,0 +1,91 @@
+using System.Runtime.Versioning;
+using Kompas.Mcp.Core.Threading;
+using Kompas6Constants;
+using KompasAPI7;
+
+namespace Kompas.Mcp.Core.Drawings;
+
+///
+/// Операции с 2D-чертежами через API7 (IViews). Все вызовы — на STA-потоке
+/// .
+///
+[SupportedOSPlatform("windows")]
+public sealed class DrawingService
+{
+ private readonly KompasSession _session;
+ private readonly KompasDispatcher _dispatcher;
+
+ public DrawingService(KompasSession session, KompasDispatcher dispatcher)
+ {
+ _session = session ?? throw new ArgumentNullException(nameof(session));
+ _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
+ }
+
+ ///
+ /// Создать на активном чертеже стандартные ассоциативные виды (спереди/сверху/слева) сохранённой
+ /// 3D-модели (.m3d/.a3d). Главный вид привязывается к точке
+ /// (,) листа, мм; масштаб — .
+ ///
+ public Task CreateStandardViewsAsync(string partFilePath, double scale,
+ double x, double y, CancellationToken ct = default)
+ => _dispatcher.InvokeAsync(() => CreateStandardViewsCore(partFilePath, scale, x, y), ct);
+
+ // Стандартная тройка проекций: ksPtFront=1, ksPtUp=3, ksPtLeft=5 (ksRelativeProjectionTypeEnum).
+ private static readonly object[] StandardProjections = { 1, 3, 5 };
+ private const int ExpectedCreated = 3;
+ private const double GapMm = 20; // зазоры между видами (DX=DY)
+
+ private DrawingViewsResult CreateStandardViewsCore(string partFilePath, double scale, double x, double y)
+ {
+ var path = DrawingValidation.NormalizeModelPath(partFilePath);
+ DrawingValidation.RequireScale(scale);
+ if (!double.IsFinite(x) || !double.IsFinite(y))
+ throw new ArgumentException("Координаты привязки вида должны быть конечными числами.");
+ if (!File.Exists(path))
+ throw new FileNotFoundException($"Файл модели не найден: {path}", path);
+ if (new FileInfo(path).Length == 0)
+ throw new InvalidOperationException($"Файл модели пуст (не сохранён?): {path}");
+
+ var views = RequireActiveDrawingViews();
+ var before = views.Count;
+
+ if (!views.AddStandartViews(path, "#Спереди", StandardProjections, x, y, scale, GapMm, GapMm))
+ throw new InvalidOperationException(
+ $"AddStandartViews вернул FALSE — виды не созданы (файл модели или проекция недоступны): {path}");
+
+ var created = views.Count - before;
+ if (created != ExpectedCreated)
+ throw new InvalidOperationException(
+ $"Ожидалось {ExpectedCreated} вида, создано {created} (было {before}, стало {views.Count}).");
+
+ // Гарантия непустоты: у спроецированных видов должна быть геометрия (иначе модель не
+ // спроецировалась — пустые виды). Системный вид #0 имеет ObjectCount 0, поэтому суммируем.
+ if (TotalObjectCount(views) <= 0)
+ throw new InvalidOperationException(
+ "Стандартные виды созданы, но пусты (модель не спроецировалась). Проверьте файл модели.");
+
+ return new DrawingViewsResult { Created = created, Total = views.Count };
+ }
+
+ private IViews RequireActiveDrawingViews()
+ {
+ var doc = _session.Application.ActiveDocument
+ ?? throw new InvalidOperationException("Нет активного документа. Создайте чертёж: document_create drawing.");
+ if (doc.DocumentType != DocumentTypeEnum.ksDocumentDrawing)
+ throw new InvalidOperationException(
+ $"Активный документ не является чертежом (тип: {doc.DocumentType}). Создайте чертёж: document_create drawing.");
+ if (doc is not IKompasDocument2D doc2d)
+ throw new InvalidOperationException("Активный документ не приводится к IKompasDocument2D.");
+ return doc2d.ViewsAndLayersManager?.Views
+ ?? throw new InvalidOperationException("Не удалось получить коллекцию видов чертежа.");
+ }
+
+ private static int TotalObjectCount(IViews views)
+ {
+ int sum = 0;
+ foreach (var o in views)
+ if (o is IView v)
+ sum += v.ObjectCount;
+ return sum;
+ }
+}
diff --git a/src/Kompas.Mcp.Core/Drawings/DrawingValidation.cs b/src/Kompas.Mcp.Core/Drawings/DrawingValidation.cs
new file mode 100644
index 0000000..bb9d0c3
--- /dev/null
+++ b/src/Kompas.Mcp.Core/Drawings/DrawingValidation.cs
@@ -0,0 +1,35 @@
+namespace Kompas.Mcp.Core.Drawings;
+
+/// Чистые валидаторы операций чертежа (без COM) — выделены для unit-тестов.
+public static class DrawingValidation
+{
+ /// Допустимые расширения файла-источника модели: деталь (.m3d) и сборка (.a3d).
+ private static readonly HashSet AllowedExtensions =
+ new(StringComparer.OrdinalIgnoreCase) { ".m3d", ".a3d" };
+
+ ///
+ /// Нормализовать путь к файлу 3D-модели: обрезать пробелы, проверить непустоту и расширение
+ /// (.m3d/.a3d), вернуть абсолютный путь. Существование файла НЕ проверяет (это делает сервис).
+ ///
+ public static string NormalizeModelPath(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path))
+ throw new ArgumentException("Путь к файлу модели не задан.", nameof(path));
+
+ var trimmed = path.Trim();
+ var ext = Path.GetExtension(trimmed);
+ if (!AllowedExtensions.Contains(ext))
+ throw new ArgumentException(
+ $"Недопустимое расширение файла модели '{ext}'. Ожидается .m3d (деталь) или .a3d (сборка).",
+ nameof(path));
+
+ return Path.GetFullPath(trimmed);
+ }
+
+ /// Проверить масштаб чертежа: конечный и строго положительный.
+ public static void RequireScale(double scale)
+ {
+ if (!double.IsFinite(scale) || scale <= 0)
+ throw new ArgumentOutOfRangeException(nameof(scale), "Масштаб должен быть конечным и > 0.");
+ }
+}
diff --git a/src/Kompas.Mcp.Core/Drawings/DrawingViewsResult.cs b/src/Kompas.Mcp.Core/Drawings/DrawingViewsResult.cs
new file mode 100644
index 0000000..336cec7
--- /dev/null
+++ b/src/Kompas.Mcp.Core/Drawings/DrawingViewsResult.cs
@@ -0,0 +1,11 @@
+namespace Kompas.Mcp.Core.Drawings;
+
+/// Результат создания стандартных видов на чертеже.
+public sealed record DrawingViewsResult
+{
+ /// Сколько видов создано этим вызовом.
+ public required int Created { get; init; }
+
+ /// Общее число видов на чертеже после операции (включая системный вид).
+ public required int Total { get; init; }
+}
diff --git a/src/Kompas.Mcp.Host/Program.cs b/src/Kompas.Mcp.Host/Program.cs
index 7b17e3a..6a047c1 100644
--- a/src/Kompas.Mcp.Host/Program.cs
+++ b/src/Kompas.Mcp.Host/Program.cs
@@ -2,6 +2,7 @@ using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Assemblies;
using Kompas.Mcp.Core.Conversion;
using Kompas.Mcp.Core.Documents;
+using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Editing;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Query;
@@ -25,6 +26,7 @@ builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
diff --git a/src/Kompas.Mcp.Host/Tools/DrawingTools.cs b/src/Kompas.Mcp.Host/Tools/DrawingTools.cs
new file mode 100644
index 0000000..2be1ec7
--- /dev/null
+++ b/src/Kompas.Mcp.Host/Tools/DrawingTools.cs
@@ -0,0 +1,28 @@
+using System.ComponentModel;
+using Kompas.Mcp.Core;
+using Kompas.Mcp.Core.Drawings;
+using ModelContextProtocol.Server;
+
+namespace Kompas.Mcp.Host.Tools;
+
+/// Инструменты 2D-чертежа (виды, оформление).
+[McpServerToolType]
+public sealed class DrawingTools(KompasSession session, DrawingService drawing)
+{
+ [McpServerTool(Name = "drawing_create_standard_views")]
+ [Description("На активном чертеже создать стандартные ассоциативные виды (спереди, сверху, слева) " +
+ "сохранённой 3D-модели (.m3d деталь / .a3d сборка). Активный документ должен быть чертежом " +
+ "(document_create drawing); модель должна быть сохранена в файл (document_save_as). " +
+ "scale — масштаб (1.0 = 1:1); x,y — точка привязки главного вида на листе (мм). " +
+ "Возвращает число созданных видов.")]
+ public async Task CreateStandardViews(
+ [Description("Путь к файлу 3D-модели (.m3d/.a3d)")] string partFilePath,
+ [Description("Масштаб видов (1.0 = 1:1)")] double scale = 1.0,
+ [Description("X привязки главного вида на листе, мм")] double x = 100,
+ [Description("Y привязки главного вида на листе, мм")] double y = 150)
+ {
+ await session.ConnectAsync();
+ var r = await drawing.CreateStandardViewsAsync(partFilePath, scale, x, y);
+ return $"Создано {r.Created} стандартных вида на чертеже (всего видов: {r.Total}, масштаб {scale}).";
+ }
+}
diff --git a/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs b/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs
new file mode 100644
index 0000000..632bf03
--- /dev/null
+++ b/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs
@@ -0,0 +1,47 @@
+using Kompas.Mcp.Core.Drawings;
+
+namespace Kompas.Mcp.Tests;
+
+[Trait("Category", "Unit")]
+public sealed class DrawingValidationTests
+{
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void NormalizeModelPath_throws_on_empty(string? path)
+ => Assert.Throws(() => DrawingValidation.NormalizeModelPath(path));
+
+ [Theory]
+ [InlineData(@"C:\m\part.step")]
+ [InlineData(@"C:\m\part.txt")]
+ [InlineData(@"C:\m\part")]
+ public void NormalizeModelPath_throws_on_wrong_extension(string path)
+ => Assert.Throws(() => DrawingValidation.NormalizeModelPath(path));
+
+ [Theory]
+ [InlineData(@"C:\m\part.m3d", ".m3d")]
+ [InlineData(@"C:\m\frame.a3d", ".a3d")]
+ [InlineData(@" C:\m\part.M3D ", ".m3d")]
+ public void NormalizeModelPath_accepts_model_files(string path, string expectedExt)
+ {
+ var result = DrawingValidation.NormalizeModelPath(path);
+ Assert.True(Path.IsPathFullyQualified(result));
+ Assert.EndsWith(expectedExt, result, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Theory]
+ [InlineData(0)]
+ [InlineData(-1)]
+ [InlineData(double.NaN)]
+ [InlineData(double.PositiveInfinity)]
+ public void RequireScale_throws_on_non_positive_or_non_finite(double scale)
+ => Assert.Throws(() => DrawingValidation.RequireScale(scale));
+
+ [Theory]
+ [InlineData(1.0)]
+ [InlineData(0.5)]
+ [InlineData(2.0)]
+ public void RequireScale_accepts_positive(double scale)
+ => Assert.Null(Record.Exception(() => DrawingValidation.RequireScale(scale)));
+}
diff --git a/tests/Kompas.Mcp.Tests/Integration/DrawingTests.cs b/tests/Kompas.Mcp.Tests/Integration/DrawingTests.cs
new file mode 100644
index 0000000..9908702
--- /dev/null
+++ b/tests/Kompas.Mcp.Tests/Integration/DrawingTests.cs
@@ -0,0 +1,88 @@
+using Kompas.Mcp.Core.Documents;
+using Kompas.Mcp.Core.Drawings;
+using Kompas.Mcp.Core.Modeling;
+
+namespace Kompas.Mcp.Tests.Integration;
+
+/// Интеграция: стандартные ассоциативные виды чертежа (API7 IViews.AddStandartViews).
+[Trait("Category", "Integration")]
+[Collection(KompasCollection.Name)]
+public sealed class DrawingTests : IntegrationTestBase
+{
+ private readonly DocumentService _docs;
+ private readonly PartModeler _modeler;
+ private readonly DrawingService _drawing;
+
+ public DrawingTests(KompasFixture fx) : base(fx)
+ {
+ _docs = new DocumentService(fx.Session, fx.Dispatcher);
+ _modeler = new PartModeler(fx.Session, fx.Dispatcher);
+ _drawing = new DrawingService(fx.Session, fx.Dispatcher);
+ }
+
+ private async Task BuildAndSaveBoxAsync()
+ {
+ var path = TestPaths.NewFile(".m3d");
+ await _docs.CreateAsync(KompasDocumentType.Part);
+ try
+ {
+ var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
+ await _modeler.AddRectangleAsync(s, 0, 0, 40, 30);
+ await _modeler.CloseSketchAsync(s);
+ await _modeler.ExtrudeAsync(s, depth: 20);
+ await _modeler.RebuildAsync();
+ await _docs.SaveAsAsync(path);
+ }
+ finally { await _docs.CloseAsync(save: false); }
+
+ Assert.True(File.Exists(path), $"Файл детали не сохранён: {path}");
+ Assert.True(new FileInfo(path).Length > 0, "Файл детали пуст.");
+ return path;
+ }
+
+ [Fact]
+ public async Task Creates_three_standard_views_on_drawing()
+ {
+ var partFile = await BuildAndSaveBoxAsync();
+
+ await _docs.CreateAsync(KompasDocumentType.Drawing);
+ try
+ {
+ var r = await _drawing.CreateStandardViewsAsync(partFile, scale: 1.0, x: 100, y: 150);
+
+ // Создано ровно 3 вида (спереди/сверху/слева); всего на чертеже +3 к системному виду.
+ Assert.Equal(3, r.Created);
+ Assert.True(r.Total >= 4, $"Ожидалось ≥4 видов всего (системный + 3), получено {r.Total}.");
+ }
+ finally { await _docs.CloseAsync(save: false); }
+ }
+
+ [Fact]
+ public async Task Rejects_non_drawing_document()
+ {
+ // Активный документ — деталь (не чертёж): понятная ошибка.
+ var partFile = await BuildAndSaveBoxAsync();
+
+ await _docs.CreateAsync(KompasDocumentType.Part);
+ try
+ {
+ await Assert.ThrowsAsync(
+ () => _drawing.CreateStandardViewsAsync(partFile, scale: 1.0, x: 100, y: 150));
+ }
+ finally { await _docs.CloseAsync(save: false); }
+ }
+
+ [Fact]
+ public async Task Missing_model_file_throws()
+ {
+ var missing = Path.Combine(TestPaths.Scratch, $"no_such_{Guid.NewGuid():N}.m3d");
+
+ await _docs.CreateAsync(KompasDocumentType.Drawing);
+ try
+ {
+ await Assert.ThrowsAsync(
+ () => _drawing.CreateStandardViewsAsync(missing, scale: 1.0, x: 100, y: 150));
+ }
+ finally { await _docs.CloseAsync(save: false); }
+ }
+}