# Дизайн: ассоциативная привязка диаметрального и радиального размеров к геометрии вида (API7)

**Дата:** 2026-05-27
**Статус:** дизайн согласован (вариант A — флаг `associate`), спайк проведён, ревью Codex спека учтено — к реализации

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

- **#3/#4** объём сужен до **окружностей** (`IDrawingContainer.Circles`): спайк подтвердил привязку
  только к окружности. Дуги (`Arcs`) — следующий инкремент со своим спайком (привязка `IArc` как
  `BaseObject` для диам./радиального не проверена; дуги одного родителя неотличимы по центру+радиусу).
- **#1** вид резолвится один раз и приводится к обоим контейнерам: `ISymbols2DContainer` (создание
  размера) И `IDrawingContainer` (поиск окружности). Новый helper `RequireViewContainers(viewNumber)`
  → `(IView, ISymbols2DContainer, IDrawingContainer)`.
- **#2** весь блок после `…Dimensions.Add()` (присвоение `BaseObject`/`Angle`/`AutoNominalValue`,
  `Update`, `Valid`, read-back) — внутри того же `try/catch` с `Delete` при любом сбое (как свободный путь).
- **#5** тест доказывает ассоциативность: ключ поиска `radius=10.3` (в допуске находит окружность R10)
  → associate вернёт Ø=20 (из геометрии), свободный дал бы 2·10.3=20.6; `Value≈20` (не 20.6) доказывает
  чтение из геометрии. + диагностический read-back `BaseObject != null`.
- **#6** обе ветки (свободная/associate) конвертируют `angle` через `DimensionAngles.ToRadians(angleDeg)`
  перед присвоением `dim.Angle`.
- **#7** пост-инвариант значения (`value > 0 && double.IsFinite`) сохраняется и в associate-ветке (иначе откат).
- **#8** регресс-тесты свободного пути (`associate=false`) для ОБОИХ — диаметрального и радиального.

## Цель

Веха 2D-ЧЕРТЁЖ, инкремент 7. Размеры пока «свободные» (значение из заданных координат). Ассоциативная
привязка делает диаметральный/радиальный размер **связанным с проекцией геометрии**: значение (Ø/R)
читается из спроецированной окружности/дуги вида через `BaseObject` и обновляется вместе с моделью.
Первый шаг к «умному» чертежу.

## Объём (обоснован рефлексией interop)

Только **диаметральный** и **радиальный**, привязка только к **окружности** (`IDrawingContainer.Circles`)
— у `IDiametralDimension`/`IRadialDimension` есть свойство `BaseObject`; `_Circle` реализует
`IDrawingObject`, поэтому объект из коллекции присваивается `dim.BaseObject` (подтверждено спайком).
У `ILineDimension` `BaseObject` НЕТ; угловой требует `BaseObject1/2`; привязка к дуге (`Arcs`) не
проверена — всё это отдельные инкременты (вне объёма).

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

Спайк (`_SpikeAssocDim`, прогнан на реальном КОМПАС, затем удалён): цилиндр (окружность R10, выдавлен
на 20) → стандартные виды. Один из видов содержит спроецированную окружность.

```
IDrawingContainer dc = (IDrawingContainer)view;   // QI от вида (как ISymbols2DContainer)
ICircles circles = dc.Circles;                     // Count=1; circles[0] as ICircle → Xc=0,Yc=0,R=10
IDiametralDimension dia = symbols.DiametralDimensions.Add();
dia.BaseObject = circles[0];   // IDrawingObject; Xc/Yc/Radius НЕ задаём
dia.Angle = π/4;               // направление выноски (размещение)
dia.Update();   // True, Valid=True
double v = ((IDimensionText)dia).NominalValue;     // == 20 — ДИАМЕТР, прочитан из геометрии!
```

**Проверено:** `BaseObject=circle` без задания координат → `Update=True`, `Valid=True`,
диаметральный `NominalValue=20` (=2·R геометрии), радиальный `NominalValue=10` (=R). `BaseObject`
остаётся непустым после. Геометрия вида: `IDrawingContainer.Circles` (`ICircle`: `Xc/Yc/Radius`),
`.Arcs` (`IArc`: `Xc/Yc/Radius/Angle1/Angle2`), `.LineSegments`. **Нет** «find by point» — выбор
перебором коллекции.

## MCP-инструменты (расширение существующих, вариант A)

К `drawing_add_diametral_dimension` и `drawing_add_radial_dimension` добавляется параметр
`associate=false`:

| `associate` | Поведение |
|---|---|
| `false` (по умолчанию) | Текущее «свободное»: `Xc/Yc/Radius` задаются напрямую (без изменений, обратная совместимость). |
| `true` | `(xc,yc,radius)` — **ключ поиска**: найти в виде окружность/дугу с центром ≈ (xc,yc) и радиусом ≈ radius, привязать `BaseObject`; значение (Ø/R) читается из геометрии. |

Координаты ключа — view-local (= координаты модели при масштабе 1:1; при ином масштабе — масштабированные,
как у прочих размеров). `angle` — направление выноски (°), как сейчас. `viewNumber` — как сейчас.

## Выбор объекта

В `IDrawingContainer` нет «find by point». Перебираем `Circles`, собираем кандидатов `(Xc, Yc, Radius)`,
ищем подходящий по ключу `(xc, yc, radius)`:
- кандидат подходит, если `dist(центр, ключ) ≤ tol` И `|R − radius| ≤ tol` (`tol = 1.0 мм`, константа);
- ровно 1 подходящий → привязываем; 0 → ошибка «окружность не найдена рядом с (xc,yc) R≈radius»;
  >1 → ошибка «неоднозначно — уточните координаты/радиус».

Радиус в ключе снимает неоднозначность концентрических окружностей (цековка R5/R10 — ключ R10
выберет только внешнюю). Логика отбора — чистая (unit-тест), перечисление COM — в сервисе.

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

- `DrawingService.AddDiametralDimensionAsync(viewNumber, xc, yc, radius, angleDeg, associate, ct)` и
  `AddRadialDimensionAsync(..., associate, ct)` — добавлен `associate` (по умолчанию `false`). При `false` —
  текущая ветка без изменений. Обе ветки: `angleRad = DimensionAngles.ToRadians(angleDeg)`.
- При `associate=true`: `RequireViewContainers(viewNumber)` → собрать кандидатов из `Circles`
  (`CollectCircles`) → `CircularObjectMatch.SelectMatchIndex(...)` → взять `IDrawingObject` по индексу →
  `symbols.{Diametral|Radial}Dimensions.Add()` → **внутри `try/catch`+`Delete`**: `dim.BaseObject = obj`,
  `dim.Angle = angleRad`, `AutoNominalValue=true` → `Update()` (FALSE→откат) → `Valid` (false→откат) →
  `value = NominalValue` (из геометрии); `value<=0 || !IsFinite` → откат → `DrawingDimensionResult`.
- Новый helper `RequireViewContainers(viewNumber)` → `(IView View, ISymbols2DContainer Symbols,
  IDrawingContainer Drawing)` — резолвит вид один раз, оба QI с раздельной диагностикой (переиспользует
  `FindView`).
- Новый файл `src/Kompas.Mcp.Core/Drawings/CircularObjectMatch.cs`: чистый статический
  `record CircleCandidate(double Xc, double Yc, double Radius)` + `int SelectMatchIndex(IReadOnlyList<CircleCandidate>
  candidates, double keyX, double keyY, double keyR, double tol)` — возвращает индекс единственного
  совпадения; `InvalidOperationException` при 0 (нет) / >1 (неоднозначно).
- Сбор кандидатов из COM (Circles в список с сохранением ссылки на `IDrawingObject` + геометрии) — в
  сервисе (private helper `CollectCircles(IDrawingContainer)` → `List<(IDrawingObject Obj, CircleCandidate Geom)>`).
- `DrawingDimensionResult` переиспользуется (значение из геометрии). Свободный путь не трогаем.

## Валидация

- Оба пути: `RequireFiniteCoords(xc, yc, angleDeg)` + `RequirePositiveRadius(radius)` (есть). В
  associate-режиме radius — положительный ключ поиска.
- `CircularObjectMatch.SelectMatchIndex` — чистая логика отбора (unit).

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

### Unit (`CircularObjectMatchTests`)
- Единственное совпадение по центру+радиусу → индекс.
- Нет кандидатов в допуске → `InvalidOperationException` (нет).
- Два совпадения в допуске → `InvalidOperationException` (неоднозначно).
- Концентрические (один центр, разный R): ключ по R выбирает нужный (не неоднозначно).
- Пустой список → `InvalidOperationException`.

### Integration (`DrawingAssociativeDimensionTests`; модель — цилиндр R10, как в спайке)
1. **Диаметральный associative + доказательство геометрии**: ключ `(0,0,10.3)` (radius в допуске
   находит окружность R10) → `Value ≈ 20` (Ø из геометрии, НЕ 2·10.3=20.6 — доказывает чтение из
   геометрии); попадание в вид (`DiametralDimensions.Count` +1).
2. **Радиальный associative**: `(0,0,10.3)` → `Value ≈ 10` (R из геометрии, не 10.3).
3. **Нет окружности рядом**: ключ далеко от геометрии → `InvalidOperationException`; счётчик не вырос (откат).
4. **Свободный путь не сломан** — диаметральный `associate=false` `(xc,yc,15)` → `Value ≈ 30`.
5. **Свободный путь не сломан** — радиальный `associate=false` `(xc,yc,12)` → `Value ≈ 12` (регресс обоих).
6. **Нет видов** (associate) → понятная ошибка.

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