# Дизайн: диаметральный размер на виде чертежа (drawing_add_diametral_dimension) через API7

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

## Правки по ревью реализации Codex

- **#2** инвариант диаметра: пост-проверка `value <= 0 || !double.IsFinite(value)` (а не `Abs<1e-6`).
- **#3** сообщение `drawing_create_standard_views` обобщено: «номера видов для инструментов размеров».
- **#4a** `DimensionAngles.ToRadians` — проверка конечности результата (защита от переполнения).
- **#5d** тест `viewNumber: 999` (виды есть, номер не найден) — покрывает ветку FindView.
- **#6** комментарий `DimensionType` переформулирован (просто COM-дефолт, без претензии на «стиль»).
- **#4b** минимальный радиус — отклонён (`>0` честный контракт). **#5a/#5b/#5c** (реальная окружность,
  read-back угла, пост-Add откат) — отклонены (инструмент geometry-agnostic; ToRadians покрыт unit;
  пост-Add сбой для radius>0 не форсировать). Транзитный сбой пакета (нестабильность КОМПАС после
  большого прогона) не воспроизводится — повторный прогон 5/5 зелёный.

## Правки по ревью Codex

- **#5** `((IDimensionText)dim).AutoNominalValue = true` выставляем явно (консистентно с линейным).
- **#7** извлечь `RequireSymbols2DContainer(viewNumber)` (= `(ISymbols2DContainer)FindView(...)`) —
  переиспользуют диаметральный путь и счётчики (радиальный — следующий, тот же паттерн).
- **#4** конверсия °→рад вынесена в чистый `DimensionAngles.ToRadians(deg)` + unit-тест (значение от
  угла не зависит → математику проверяем отдельно).
- **#2** `DimensionType` НЕ задаём — принимаем COM-дефолт (стандартный стиль; полярность bool не
  подтверждена, спайк с дефолтом дал `Valid` + верное значение). Зафиксировано комментарием.
- **#1** размер «свободный» (по Xc/Yc/Radius, без `BaseObject`) — как у линейного; LLM задаёт верные
  параметры окружности. Ассоциативная привязка — будущее. Отражено в описании инструмента.
- **#3** `angle` — любой конечный (направление выноски, КОМПАС нормализует); валидируем лишь
  конечность. **#6** откат после `Add()` для диаметрального не форсируется (radius>0 → всегда Valid,
  value=2R>0); код отката идентичен протестированному линейному (zero-projection тест).

## Цель

Веха 2D-ЧЕРТЁЖ, инкремент 4. После линейного размера — **диаметральный** размер окружности на виде
(Ø для отверстий/цилиндров — частый случай конструкторского чертежа). Радиальный (с ветвями) и
угловой — следующие инкременты.

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

Та же модель, что у линейного размера (`ISymbols2DContainer` QI от вида). Спайк
(`DrawingDiametralSpikeTests`, зелёный):

```
ISymbols2DContainer symbols = (ISymbols2DContainer)view;   // QI от конкретного IView
IDiametralDimension dim = symbols.DiametralDimensions.Add();
dim.Xc = xc; dim.Yc = yc;        // центр окружности (ЛОКАЛЬНАЯ СК вида, мм)
dim.Radius = radius;             // радиус окружности (мм)
dim.Angle = angleRad;            // направление выноски (РАДИАНЫ)
bool ok = dim.Update();
double value = ((IDimensionText)dim).NominalValue;   // = ДИАМЕТР (2·Radius)
```

**Проверено:** `Xc=10,Yc=10,Radius=15,Angle=0.785` → `Update=True`, `Valid=True`,
`NominalValue == 30` (= диаметр = 2·Radius). `Angle` — в **радианах** (конвенция 2D-API КОМПАС;
для размера задаёт лишь направление выноски, на значение не влияет).

**Координаты** — локальная СК вида (как у линейного). Размер «свободный» (по Xc/Yc/Radius), без
ассоциативной привязки к геометрии (`BaseObject` не задаём — будущее).

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

| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_add_diametral_dimension` | `xc,yc` (центр), `radius`, `angle=0` (°), `viewNumber=0` | Поставить диаметральный размер (Ø) окружности с центром (xc,yc) и радиусом `radius` (ЛОКАЛЬНАЯ СК вида, мм) на виде. `angle` — направление выноски в ГРАДУСАХ (внутри → радианы). `viewNumber`: 0 = первый/главный вид. Значение (диаметр = 2·radius) измеряется автоматически. Возвращает диаметр и номер вида. |

`angle` экспонируется в **градусах** (LLM-дружелюбно), внутри умножается на π/180.

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

В существующем `DrawingService` (рядом с `AddLinearDimensionAsync`); переиспользуем `FindView`,
`DrawingDimensionResult`, паттерн отката.

- `DrawingService.AddDiametralDimensionAsync(viewNumber, xc, yc, radius, angleDeg, ct)`.
- Инструмент `drawing_add_diametral_dimension` в `DrawingTools`.
- Helper `GetViewDiametralDimensionCountAsync` (для теста — попадание в вид; размеры в
  `ISymbols2DContainer`, не в `ObjectCount` — как выяснено для линейных).

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

- `DrawingValidation.RequireFiniteCoords(xc, yc, angleDeg)` (уже есть).
- `DrawingValidation.RequirePositiveRadius(double radius)` (новый) — конечный и `> 0`.

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

`AddDiametralDimensionCore`: валидация (coords + radius>0) → `FindView` → `(ISymbols2DContainer)view`
→ `DiametralDimensions.Add()` (null-check) → `Xc/Yc/Radius`, `Angle = angleDeg·π/180` → `Update()`
(FALSE → откат `dim.Delete` + ошибка) → `Valid` (false → откат) → `value = ((IDimensionText)dim).
NominalValue` (диаметр); если `≤ 0` → откат + ошибка → вернуть `DrawingDimensionResult { Value, ViewNumber }`.
RCW точечно не освобождаем (v2-2).

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

### Unit (`DrawingValidationTests`)
- `RequirePositiveRadius` — бросает на 0/отрицательном/NaN/Infinity; пропускает положительный.

### Integration (`DrawingDiametralTests`)
1. **Значение = диаметр**: Radius=15 → `Value ≈ 30`.
2. **Попадание в вид**: `*DiametralDimensions.Count` целевого вида +1 (адресация по `viewNumber` из
   `ViewNumbers`).
3. **Радиус ≤ 0** → `ArgumentOutOfRangeException` (откат не нужен — до `Add`).
4. **Нет видов** → понятная ошибка.

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

## Дальнейшее (вне спека)
- Радиальный размер (`RadialDimensions`, ветви), угловой; ассоциативная привязка к окружности вида
  (`BaseObject`); рамка/формат листа.
