# Дизайн: формат и ориентация листа чертежа (drawing_set_sheet_format) через API7

**Дата:** 2026-05-27
**Статус:** дизайн согласован, спайк проведён, ревью pi/glm-5.1 + pi/kimi-k2.6 учтено — к реализации

## Правки по ревью реализации (Codex + pi/glm-5.1 + pi/kimi-k2.6)

- **K1 (kimi, Major)** — опасение, что для `User` read-back ориентации устаревший (VerticalOrientation
  не задаётся). **Снято спайком:** КОМПАС САМ выводит `VerticalOrientation` из соотношения W/H
  (500×300→альбомная, 300×500→книжная), игнорирует заданный флаг и НЕ свопает W/H → read-back верный,
  код корректен. Документировано комментарием; добавлены тесты на `Landscape` для User (обе ориентации).
- **K2** — `ValidateFormatDimensions` теперь называет точный нарушивший параметр (`width`/`height`).
- **C1/G1/K3** — тесты User проверяют `Landscape`. **C2/G3** — тест `sheetNumber=0`→`ArgumentOutOfRangeException`.
  **C3** — A1 в `Parse`-тесте. **G2** — тест A4 landscape (своп 297×210).
- Отклонено (нит): **G4** алиасы в описании (lenient parse — намеренно), **G5** сообщение при NaN
  (для стандартного «размеры только для user» приемлемо), **K5** тип исключения для стандартного формата (ArgumentException
  для «неверная комбинация» vs AOORE для «вне диапазона» — намеренное различие, на нём держатся тесты),
  **G6** «нет unit для ValidateFormatDimensions» — ложно, покрыто в `DrawingValidationTests`.

## Правки по ревью спека (pi/glm-5.1 + pi/kimi-k2.6)

- **A** для `user` флаг `landscape` **игнорируется** — ориентация задаётся соотношением `width`/`height`
  (КОМПАС сам выставит `VerticalOrientation`). `VerticalOrientation=!landscape` ставим ТОЛЬКО для
  стандартных форматов (избегаем неоднозначного свопа W/H). Задокументировано в описании инструмента.
- **B/E** `width`/`height` для стандартного формата запрещены: валидатор `ValidateFormatDimensions`
  (для `User` — оба конечны и `>0`; для стандартного — оба `==0`, иначе `ArgumentException` «размеры
  только для user»).
- **C** read-back формата возвращает дружелюбное имя через новый `PaperFormats.FromKompas(ksEnum)` →
  `SheetFormatResult.Format` = `"A3"`/`"User"` (симметрия вход/выход), а не COM-имя `"ksFormatA3"`.
- **D** `RequireLayoutSheet(sheetNumber)`: `sheetNumber>0` (`ArgumentOutOfRangeException`) + null-check
  после `ItemByNumber[sheetNumber]` (`InvalidOperationException` «нет листа N»); + интеграционный тест.
- **F** read-back `fmt2 = sheet.Format` с null-check.
- **G** `SheetFormatResult` — `public sealed record` с `public required … { get; init; }` (как
  `DrawingDimensionResult`/`DrawingViewsResult`).
- **H** интеграционные сравнения размеров — через `Assert.InRange` (допуск на float).
- **I** члены `PaperFormat` — с XML-`<summary>` (как `RoughSignType`).
- **J** `FormatMultiplicity` оставляем на COM-дефолте (1) — отмечено в реализации.
- **K** (верхняя граница `width`/`height`) — отклонено, в OPEN_QUESTIONS как будущее упрочнение
  (консистентно с `RequirePositiveRadius` — верхней границы нет).

## Цель

Веха 2D-ЧЕРТЁЖ, инкремент 8. Задать **формат** (A0–A5 или пользовательский) и **ориентацию**
(книжная/альбомная) листа активного чертежа. Фундамент «правильного» по ГОСТ чертежа (сейчас лист
всегда дефолтный A4 книжный).

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

Спайк (`_SpikeSheetFormat`, прогнан на реальном КОМПАС, затем удалён): новый чертёж →
`doc.LayoutSheets.ItemByNumber[1]` (`ILayoutSheet`) → `sheet.Format` (`ISheetFormat`).

```
ISheetFormat fmt = sheet.Format;
fmt.Format = ksDocumentFormatEnum.ksFormatA3;   // A0=0..A5=5, User=6
fmt.VerticalOrientation = false;                 // true=книжная (portrait), false=альбомная (landscape)
sheet.Update();   // True
// для стандартного формата FormatWidth/Height пересчитываются АВТО (A3 landscape → 420×297)
```

**Проверено:**
- Дефолт нового чертежа: `ksFormatA4`, `VerticalOrientation=true`, 210×297, `FormatMultiplicity=1`.
- A3 landscape: `Update=True`, read-back `Format=ksFormatA3`, `Vertical=False`, **W=420, H=297 (авто)** —
  для стандартных форматов размеры не задаём, их даёт enum+ориентация.
- A4 portrait: 210×297.
- Пользовательский: `Format=ksFormatUser` + `FormatWidth=500` + `FormatHeight=300` + `Update` → применён.

`ksDocumentFormatEnum`: `ksFormatA0=0`, `A1=1`, `A2=2`, `A3=3`, `A4=4`, `A5=5`, `ksFormatUser=6`.
`ISheetFormat`: `Format` (enum), `VerticalOrientation` (bool), `FormatWidth`/`FormatHeight` (double, мм),
`FormatMultiplicity` (int).

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

| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_set_sheet_format` | `format="A4"` (A0\|A1\|A2\|A3\|A4\|A5\|user), `landscape=false` (false=книжная), `width=0`,`height=0` (ТОЛЬКО для `user`, мм, >0), `sheetNumber=1` | Задать формат и ориентацию листа `sheetNumber` активного чертежа. Стандартный формат — размеры авто (по enum+ориентации), `width`/`height` для него запрещены (должны быть 0). `user` — задать `width`/`height` (>0); для `user` флаг `landscape` ИГНОРИРУЕТСЯ (ориентация — из соотношения сторон). Возвращает итоговый формат («A3»/«User»), ширину, высоту, ориентацию (read-back). |

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

В `DrawingService` (namespace `Kompas.Mcp.Core.Drawings`).
- `SetSheetFormatAsync(PaperFormat format, bool landscape, double width, double height, int sheetNumber, ct)`
  → `SheetFormatResult`.
- Helper `RequireLayoutSheet(sheetNumber)` → `ILayoutSheet`: `sheetNumber>0`
  (`ArgumentOutOfRangeException`) → нет активного чертежа / `LayoutSheets` null / `ItemByNumber[sheetNumber]`
  null («нет листа N») — раздельная диагностика, рядом с `RequireActiveStamp`.
- Новый файл `src/Kompas.Mcp.Core/Drawings/PaperFormat.cs`: `enum PaperFormat {A0,A1,A2,A3,A4,A5,User}`
  (каждый член с XML-`<summary>`) + `PaperFormats.Parse(string)` / `ToKompas(PaperFormat)→ksDocumentFormatEnum`
  / `FromKompas(ksDocumentFormatEnum)→PaperFormat` (для read-back дружелюбного имени), по образцу `RoughSignTypes`.
- Новый файл `src/Kompas.Mcp.Core/Drawings/SheetFormatResult.cs`:
  `public sealed record SheetFormatResult { public required string Format; public required double Width;
  public required double Height; public required bool Landscape; }` (стиль `DrawingDimensionResult`).

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

`SetSheetFormatCore`: `DrawingValidation.ValidateFormatDimensions(format, width, height)` →
`PaperFormats.ToKompas(format)` → `RequireLayoutSheet(sheetNumber)` → `fmt = sheet.Format` (null-check) →
`fmt.Format = ksEnum`; if `format==User`: `fmt.FormatWidth=width`, `fmt.FormatHeight=height` (ориентацию
НЕ трогаем — задаётся W/H); else: `fmt.VerticalOrientation = !landscape` (размеры авто) → `sheet.Update()`
(FALSE → ошибка; объект уровня листа, перезапись идемпотентна — отката `Delete` нет) → read-back
`fmt2 = sheet.Format` (null-check) → `SheetFormatResult { Format = PaperFormats.FromKompas(fmt2.Format)
.ToString(), Width = fmt2.FormatWidth, Height = fmt2.FormatHeight, Landscape = !fmt2.VerticalOrientation }`.

`FormatMultiplicity` оставляем на COM-дефолте (1) — не читаем и не задаём. RCW точечно не освобождаем
(консистентно; долг v2-2).

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

- `PaperFormats.Parse` — `A0..A5`/`user` (+ регистр/пробелы); неизвестное → `ArgumentException`; null → `ArgumentNullException`.
- Новый `DrawingValidation.ValidateFormatDimensions(PaperFormat format, double width, double height)`:
  для `User` — `width` и `height` оба конечны и `> 0` (иначе `ArgumentOutOfRangeException`); для стандартного —
  `width==0 && height==0` (иначе `ArgumentException` «размеры задаются только для format=user»).
- `PaperFormats.ToKompas`/`FromKompas` (↔ `ksDocumentFormatEnum`) НЕ покрываем unit — граница проекта
  (тест не ссылается на `Kompas6Constants`), как у прочих enum-маппингов; покрываются интеграцией.

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

### Unit (`PaperFormatsTests`, `DrawingValidationTests`)
- `PaperFormats.Parse`: `A4/a3/ A2 /a0/a5/user` → enum; неизвестное → `ArgumentException`; null → `ArgumentNullException`.
- `ValidateFormatDimensions`:
  - `User`: бросает на (0,300)/(500,0)/(−1,300)/(NaN,300)/(500,∞); пропускает (500,300).
  - стандартный (`A4`): бросает на (500,0)/(0,300)/(500,300); пропускает (0,0).

### Integration (`DrawingSheetFormatTests`; сравнения размеров — `Assert.InRange`)
1. **A3 landscape**: `format=A3, landscape=true` → `Format=="A3"`, `Width≈420`, `Height≈297`, `Landscape==true`.
2. **A4 portrait** (`landscape=false`): → `Format=="A4"`, `Width≈210`, `Height≈297`, `Landscape==false`.
3. **User 500×300**: `format=user, width=500, height=300` → `Format=="User"`, `Width≈500`, `Height≈300`.
4. **User без размеров** (`width=0`) → `ArgumentOutOfRangeException` (до COM).
5. **Размеры для стандартного** (`format=A3, width=400`) → `ArgumentException` (до COM).
6. **Несуществующий лист** (`sheetNumber=99`) → `InvalidOperationException`.
7. **Активный документ не чертёж** (деталь) → понятная ошибка.

## Дальнейшее (вне спека)
- Рамка/основная надпись по конкретному ГОСТ-стилю оформления (`LayoutLibraryFileName`/`LayoutStyleNumber`),
  несколько листов (`LayoutSheets.Add`); привязка к дуге, угловой/шероховатость; выноски/базы/допуски формы.
