# Дизайн: стандартные виды чертежа (drawing_create_standard_views) через API7

**Дата:** 2026-05-27
**Статус:** реализовано и проверено (3 интеграционных + 18 unit, всего 177); ревью Codex спека (10) и реализации (7) учтено

## Правки по ревью реализации Codex (7 замечаний)

- **#1** непустоту проверяем у ВНОВЬ созданных видов (по разнице `IView.Number` до/после), а не по
  всей коллекции — на непустом чертеже прежняя геометрия не маскирует пустые новые виды.
- **#3** откат: при `created != 3` или пустых видах удаляем созданные виды (`IView.Delete`) → чертёж
  не остаётся в полу-состоянии.
- **#2** ленивость `ObjectCount` опровергнута эмпирически (happy-тест зелёный = геометрия есть сразу,
  rebuild не нужен). **#7** добавлен unit-тест нормализации кириллического пути.
- **#4** повторный вызов намеренно добавляет ещё набор (дубликаты в разных `x,y` легитимны) —
  отражено в описании инструмента. **#5** обход `IViews` уточнён (по `Number`); точечный release RCW
  отклонён (v2-2). **#6** доп. негативы (.a3d, пустой валидный .m3d) — маргинальны, отложены.

## Решения по ревью 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` не тестируем — `"#Спереди"` захардкожен, не управляется пользователем.

## Цель

Веха 2 — **2D-ЧЕРТЁЖ** (новый класс функционала после деталей и сборок). Инкремент 1: создать на
активном чертеже **стандартные ассоциативные виды** (спереди/сверху/слева) по сохранённой 3D-модели
`.m3d`/`.a3d`. Это фундамент чертежа — без видов лист пуст. Чертёж проверяется не объёмом, а
**числом созданных видов**.

## Спайк: механизм подтверждён вживую (НЕ разучивать)

Спайк (`DrawingSpikeTests`, зелёный) подтвердил путь API7:

```
IKompasDocument2D doc2d = (IKompasDocument2D)app.ActiveDocument;   // активный чертёж
IViews views = doc2d.ViewsAndLayersManager.Views;                 // COM-свойства (не QI)
object[] projTypes = { 1, 3, 5 };  // ksPtFront=1, ksPtUp=3, ksPtLeft=5 (SAFEARRAY VT_I4)
bool ok = views.AddStandartViews(path, "#Спереди", projTypes, X:100, Y:150, Scale:1.0, DX:20, DY:20);
int count = views.Count;
```

**Проверенные факты:**
- `object[] { 1, 3, 5 }` **маршалится в SAFEARRAY VT_I4 без проблем** (риск из ревью SDK снят).
- Новый чертёж стартует с `Views.Count == 1` (системный вид #0). После `AddStandartViews` с тремя
  кодами проекций `Count == 4` → **создано ровно 3 вида** (спереди/сверху/слева). `ok == True`.
- `ProjectionName = "#Спереди"` — ориентация главного вида (стандартное имя проекции 3D-модели).
- `IKompasDocument2D` напрямую (без QI к `IKompasDocument2D1`) даёт `ViewsAndLayersManager.Views`
  (подтверждено рефлексией interop — отличие от ревью SDK).
- Явный rebuild/save после `AddStandartViews` НЕ нужен — виды уже в документе (`Count` сразу 4).

**Параметры `AddStandartViews(FileName, ProjectionName, ProjectionsTypes, X, Y, Scale, DX, DY)`:**
- `FileName` — путь к `.m3d`/`.a3d`.
- `ProjectionName` — ориентация главного вида (`"#Спереди"`).
- `ProjectionsTypes` — `SAFEARRAY VT_I4` кодов `ksRelativeProjectionTypeEnum`: `ksPtFront=1`,
  `ksPtRear=2`, `ksPtUp=3`, `ksPtDown=4`, `ksPtLeft=5`, `ksPtRight=6`, `ksPtIsoXYZ=7`.
- `X, Y` — точка привязки главного вида на листе, мм.
- `Scale` — масштаб (1.0 = 1:1).
- `DX, DY` — зазоры между видами, мм. Возврат: `bool` (успех).

## Архитектура

Новый класс (как сборки — отдельный от `PartModeler`). Сервис `DrawingService` (API7), инструменты
`DrawingTools`, namespace `Kompas.Mcp.Core.Drawings`.

- `src/Kompas.Mcp.Core/Drawings/DrawingService.cs` — сервис.
- `src/Kompas.Mcp.Core/Drawings/DrawingValidation.cs` — чистые static-валидаторы (unit).
- `src/Kompas.Mcp.Host/Tools/DrawingTools.cs` — MCP-инструмент.
- DI: `AddSingleton<DrawingService>()` в `Program.cs`.

## MCP-инструмент

| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_create_standard_views` | `partFilePath: string`, `scale=1.0` | На активном чертеже создать стандартные ассоциативные виды (спереди/сверху/слева) сохранённой детали/сборки. Возвращает число созданных видов. |

Контракт: активный документ — чертёж (`ksDocumentDrawing`); файл модели существует. Набор видов
фиксирован (спереди+сверху+слева, 3 вида); позиция/зазоры — дефолты (X=100, Y=150, DX=20, DY=20 мм).
Выбор набора видов / позиции / изометрии — будущие инкременты (YAGNI).

## Валидация (чистые static, unit-тест)

`DrawingValidation`:
- `NormalizeModelPath(string? path)` — `Trim` → непустота → расширение `.m3d`/`.a3d` → `Path.GetFullPath`.
- `RequireScale(double scale)` — конечный и `> 0`.

`File.Exists` — в сервисе (`FileNotFoundException` с абсолютным путём).

## Проверка активного документа

`RequireActiveDrawing()` — раздельные ошибки: нет документа / тип != `ksDocumentDrawing` / не
приводится к `IKompasDocument2D`.

## Реализация

`CreateStandardViewsAsync(partFilePath, scale, ct)` — на STA-потоке: валидация (`NormalizeModelPath`,
`RequireScale`) → `File.Exists` → `RequireActiveDrawing` → `views.Count` (before) →
`AddStandartViews(path, "#Спереди", {1,3,5}, 100, 150, scale, 20, 20)` (результат проверяем: FALSE →
ошибка) → `created = views.Count - before` (ожидаем 3; если 0 — ошибка «виды не созданы»). Возврат —
число созданных видов. Транзитные RCW не освобождаем точечно (долг v2-2).

## Тестирование

### Unit (`DrawingValidationTests`)
- `NormalizeModelPath` — бросает на пустом/чужом расширении (`.step`); принимает `.m3d`/`.a3d`
  (регистр, пробелы), возвращает абсолютный путь.
- `RequireScale` — бросает на 0/отрицательном/NaN/Infinity; принимает положительный.

### Integration (`DrawingTests`)
Паттерн: построить деталь 40×30×20 → `SaveAsAsync(.m3d)` → проверить файл → `CloseAsync` → создать
чертёж → `CreateStandardViewsAsync` → проверить → `finally CloseAsync` (база `IntegrationTestBase`).
1. **Создание видов**: `AddStandartViews` → возврат `== 3`; `Views.Count` вырос на 3 (с 1 до 4).
2. **Не-чертёж**: активный документ — деталь → `CreateStandardViewsAsync` бросает (понятная ошибка).

(Сценарий 1 = промотированный спайк; спайк-файл удаляется.)

## Дальнейшее (вне спека)
- Выбор набора видов (+изометрия, +сечения), позиция/масштаб листа, размеры/обозначения на видах,
  рамка/основная надпись.
