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

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

## Уточнение по реализации (важно)

Спайк изначально решил, что измеряемый сектор выбирает положение дуги `(x3,y3)`. **При реализации
выяснилось иначе:** какой угол измерять, выбирает **`angleType`** (`ksADMinAngle`/`MaxAngle`/`MoreAngle`),
а `(x3,y3)` лишь позиционирует размерную дугу и задаёт её радиус. (270° в спайке возникал потому, что
`X3/Y3` не были заданы = совпадали с вершиной → вырождение.)

Три типа дают **три разных угла** (проверено на лучах 0° и 45°):
- `Min` (`ksADMinAngle`) → **45°** (острый);
- `Max` (`ksADMaxAngle`) → **135°** (тупой супплемент, 180−45);
- `More` (`ksADMoreAngle`) → **315°** (рефлексный, 360−45).

Тесты (#8 More→270 на 90°, Max→135 на 45°) и описания инструмента/сервиса исправлены под это.

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

- **#1** описание параметра `x3,y3` в инструменте переформулировано (точка позиционирует дугу; угол
  выбирает `angleType`, не она).
- **#2** сообщение пост-проверки углового больше не винит `(x3,y3)` — указывает на коллинеарность сторон.
- **#3** добавлен интеграционный тест `angleType=Max` (лучи 0°/45° → 135°) — покрыл маппинг `ksADMaxAngle`,
  отличный от `Min`/`More`.
- **#4** (откат радиального не тестируется) — **отклонено с обоснованием:** как у диаметрального
  (`radius>0` → всегда `Valid`, `value=radius>0`; детерминированного пост-Add сбоя нет), код отката
  структурно идентичен протестированному угловому (коллинеарный путь, счётчик не меняется).

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

- **#1** угловой: добавлен `RequireDistinctPoints(x1,y1,x2,y2)` (совпавшие точки сторон → одинаковые
  лучи → вырождение ещё до `Add`).
- **#3** добавлен тест отката после `Add`: коллинеарные сонаправленные лучи (вершина(0,0),(10,0),(20,0))
  → угол ≈0 → пост-проверка `value<=0` → откат `Delete` + ошибка; счётчик угловых не изменился.
- **#2** поведение `x3,y3` (выбор сектора) задокументировано в описании инструмента; добавлен
  интеграционный тест выбора сектора (та же геометрия, `x3,y3` в дополнительном секторе → ~270°).
  Большой/тупой угол НЕ отвергаем — это легитимный результат (рефлексный угол через `more`/положение дуги).
- **#4** добавлены тесты на несуществующий `viewNumber>0` (ветка `FindView` «вид не найден») для обоих.
- **#5** `DrawingDimensionResult.Value` — XML-док станет единице-нейтральным («значение размера»);
  ответ `drawing_add_angular_dimension` форматируется в градусах (°), не «мм».
- **#6** радиальный использует `DimensionAngles.ToRadians(angleDeg)` (как диаметральный), не ручное `·π/180`.

## Цель

Веха 2D-ЧЕРТЁЖ, инкремент 5. После линейного и диаметрального размеров — **радиальный (R)** и
**угловой** размеры на виде. Завершают базовое семейство размеров чертежа. Оба переиспользуют
проверенный путь `ISymbols2DContainer` (QI от `IView`) и паттерн «Add → set → Update → проверка
`Valid` → чтение `NominalValue` → откат `Delete` при ошибке».

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

Спайк (`_SpikeDimensions`, прогнан на реальном КОМПАС, затем удалён) на коробке 40×30×20 с
тремя стандартными видами. Контейнер размеров — `(ISymbols2DContainer)view`.

### Радиальный

```
IRadialDimension rd = symbols.RadialDimensions.Add();
rd.Xc = 10; rd.Yc = 10; rd.Radius = 15; rd.Angle = π/4;   // Angle — РАДИАНЫ (направление выноски)
rd.DimensionType = true;                                   // BOOL: на значение НЕ влияет (стиль)
((IDimensionText)rd).AutoNominalValue = true;
rd.Update();  // True, rd.Valid == True
double value = ((IDimensionText)rd).NominalValue;          // == 15 — РАДИУС (не диаметр!)
```

**Ключевой факт (расходится со справкой SDK):** у радиального `NominalValue` возвращает **радиус**
(`Radius=15 → NominalValue=15`), а НЕ диаметр. Проверено для `DimensionType` и `true`, и `false` —
оба `Valid`, значение одинаковое (BOOL влияет лишь на стиль «от центра / нет», не на величину).
Координаты — локальная СК вида, мм. Размер «свободный» (по `Xc/Yc/Radius`, без `BaseObject`).

### Угловой

```
IAngleDimension ad = symbols.AngleDimensions.Add(DrawingObjectTypeEnum.ksDrADimension);  // =10
ad.Xc = 0; ad.Yc = 0;        // вершина угла
ad.X1 = 10; ad.Y1 = 0;       // точка на стороне 1  → луч (Xc,Yc)→(X1,Y1)
ad.X2 = 0;  ad.Y2 = 10;      // точка на стороне 2  → луч (Xc,Yc)→(X2,Y2)
ad.X3 = 5;  ad.Y3 = 5;       // положение размерной дуги — ЗАДАЁТ ИЗМЕРЯЕМЫЙ СЕКТОР + радиус дуги
ad.DimensionType = ksADMinAngle;  // 0=min / 1=max / 2=more180
((IDimensionText)ad).AutoNominalValue = true;
ad.Update();  // True, ad.Valid == True
double value = ((IDimensionText)ad).NominalValue;          // == 90 — ГРАДУСЫ
```

**Проверено:**
- вершина(0,0), точки (10,0)&(0,10), `X3/Y3`=(5,5) → **90°** ✓;
- вершина(0,0), точки (10,0)&(10,10), `X3/Y3`=(8,4) → **45°** ✓.
- **`X3/Y3` критичен:** без него тот же угол измерился как **270°** (дополнительный сектор). Точка
  положения дуги задаёт, какой из двух секторов меряется; заодно радиус дуги пересчитывается в
  `dist(вершина, X3/Y3)` (заданный `Radius` перетирается → его не экспонируем).
- Подход через `Angle1/Angle2` (углы наклона сторон) — **вырожденный** (точки схлопываются к
  вершине, `X1≈0.001`), не используем. Геометрию ведут только координаты точек.
- `NominalValue` — в **градусах**.

`Add(DrawingObjectTypeEnum)` требует код типа: `ksDrADimension = 10` (обычный угловой),
`ksDrABreakDimension = 39` (с обрывом — вне объёма).

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

| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_add_radial_dimension` | `xc,yc` (центр окружности), `radius`, `angle=0` (°), `viewNumber=0` | Радиальный размер (R) окружности/дуги с центром (xc,yc) и радиусом `radius` (ЛСК вида, мм). `angle` — направление выноски в ГРАДУСАХ (внутри → радианы). `viewNumber`: 0 = первый/главный. Значение (= радиус) измеряется автоматически. Возвращает радиус и номер вида. |
| `drawing_add_angular_dimension` | `xc,yc` (вершина), `x1,y1` (точка на стороне 1), `x2,y2` (точка на стороне 2), `x3,y3` (положение размерной дуги — задаёт сектор), `angleType="min"`, `viewNumber=0` | Угловой размер между лучами (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) (ЛСК вида, мм). `x3,y3` должны лежать в измеряемом секторе. `angleType`: min/max/more. Значение (градусы) измеряется автоматически. Возвращает угол (°) и номер вида. |

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

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

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

- `DrawingService.AddRadialDimensionAsync(viewNumber, xc, yc, radius, angleDeg, ct)`.
- `DrawingService.AddAngularDimensionAsync(viewNumber, xc, yc, x1, y1, x2, y2, x3, y3, angleType, ct)`.
- Инструменты `drawing_add_radial_dimension`, `drawing_add_angular_dimension` в `DrawingTools`.
- Helpers `GetViewRadialDimensionCountAsync`, `GetViewAngularDimensionCountAsync` (для тестов —
  попадание в вид; размеры в `ISymbols2DContainer.RadialDimensions/AngleDimensions.Count`, не в
  `IView.ObjectCount`).
- Новый файл `src/Kompas.Mcp.Core/Drawings/AngleDimensionType.cs`: `enum AngleDimensionType {Min,Max,More}`
  + static `AngleDimensionTypes.Parse(string)` / `ToKompas(...)` (→ `ksADMinAngle/ksADMaxAngle/ksADMoreAngle`),
  по образцу `DimensionOrientations`.

`DrawingDimensionResult` (Value + ViewNumber) переиспользуется: радиальный `Value`=радиус, угловой
`Value`=градусы.

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

- Радиальный: `RequireFiniteCoords(xc, yc, angleDeg)` + `RequirePositiveRadius(radius)` — уже есть.
- Угловой: `RequireFiniteCoords(xc,yc,x1,y1,x2,y2,x3,y3)` (8 координат) + `RequireDistinctPoints`
  четырежды: вершина≠(x1,y1), вершина≠(x2,y2), вершина≠(x3,y3) (иначе дуга нулевого радиуса),
  (x1,y1)≠(x2,y2) (#1: совпавшие точки сторон → одинаковые лучи). Все уже есть; новых валидаторов не требуется.

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

**`AddRadialDimensionCore`** (зеркало диаметрального, но значение = радиус): валидация → `RequireSymbols2DContainer`
→ `RadialDimensions.Add()` (null-check) → `Xc/Yc/Radius`, `Angle=DimensionAngles.ToRadians(angleDeg)` (#6), `DimensionType=true`
(стиль R от центра, явно), `AutoNominalValue=true` → `Update()` (FALSE → откат `Delete`+ошибка) →
`Valid` (false → откат) → `value = NominalValue` (радиус); `value<=0 || !IsFinite` → откат+ошибка →
`DrawingDimensionResult{Value, ViewNumber}`.

**`AddAngularDimensionCore`**: валидация → `RequireSymbols2DContainer` → `AngleDimensions.Add(ksDrADimension)`
(null-check) → `Xc/Yc/X1/Y1/X2/Y2/X3/Y3`, `DimensionType=AngleDimensionTypes.ToKompas(angleType)`,
`AutoNominalValue=true` → `Update()` (FALSE → откат) → `Valid` (false → откат) → `value=NominalValue`
(°); `value<=0 || !IsFinite` → откат+ошибка с подсказкой (вырожденный угол: стороны коллинеарны или
`x3,y3` вне сектора) → `DrawingDimensionResult{Value, ViewNumber}`. Угол в (0,360); защиту по верхней
границе не форсируем (КОМПАС нормализует, `more` может дать >180).

RCW точечно не освобождаем (консистентно с линейным/диаметральным; долг v2-2).

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

### Unit (`DrawingDimensionTypeTests` — новый, либо в `DrawingValidationTests`)
- `AngleDimensionTypes.Parse`: `min/max/more` (+ рус. синонимы) → enum; неизвестное → `ArgumentException`.
- `AngleDimensionTypes.ToKompas`: маппинг трёх значений → `ksADMinAngle/ksADMaxAngle/ksADMoreAngle`.
- (`ToRadians`, `RequirePositiveRadius`, координаты — уже покрыты.)

### Integration (`DrawingRadialTests`, `DrawingAngularTests`; наследуют `IntegrationTestBase`)
Радиальный:
1. **Значение = радиус**: `radius=12 → Value ≈ 12` (промотированный спайк).
2. **Попадание в вид**: `RadialDimensions.Count` целевого вида +1 (адресация по `viewNumber`).
3. **Радиус ≤ 0** → `ArgumentOutOfRangeException` (до `Add`).
4. **Нет видов** → понятная ошибка.

Угловой:
5. **Измеряет 90°**: вершина(0,0), (10,0)&(0,10), x3/y3=(5,5) → `Value ≈ 90`.
6. **Измеряет 45°**: вершина(0,0), (10,0)&(10,10), x3/y3=(8,4) → `Value ≈ 45`.
7. **Попадание в вид**: `AngleDimensions.Count` целевого вида +1.
8. **Выбор угла типом** (#2): `angleType=More` на геометрии 90° → `Value ≈ 270`; `angleType=Max`
   на лучах 0°/45° → `Value ≈ 135` (тупой супплемент). Какой угол измерять, выбирает `angleType`, не `x3,y3`.
9. **Вырожденный угол до Add** (вершина совпадает с точкой стороны) → `ArgumentException`.
10. **Откат после Add** (#3): коллинеарные сонаправленные лучи (0,0),(10,0),(20,0) → угол ≈0 →
    `InvalidOperationException`; `AngleDimensions.Count` целевого вида НЕ изменился (откат `Delete`).
11. **Несуществующий `viewNumber>0`** (#4) → понятная ошибка (ветка `FindView`).
12. **Нет видов** → понятная ошибка.

(Радиальный — аналогично добавлен тест на несуществующий `viewNumber>0`, #4.)

## Дальнейшее (вне спека)
- Ассоциативная привязка размеров к геометрии вида (`BaseObject`/`BaseObject1/2`); рамка/формат листа;
  текстовые обозначения (шероховатость `Roughs`, допуски формы `Tolerances`, выноски `Leaders`,
  тех. требования); радиальный с изломом (`BreakRadialDimensions`), угловой с обрывом (`ksDrABreakDimension`).
