# Дизайн: линия-выноска с текстом (drawing_add_leader) через API7

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

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

- Проверка возврата `AddBranchByPoint`/`SetBranchTextPosition` (оба `bool`) — ранняя диагностика
  вместо позднего `RPC_E_SERVERFAULT` (Codex, glm).
- RU-алиасы в сообщении `ShelfDirections.Parse` и в `[Description]` инструмента (glm, kimi).
- Интеграционный тест направления полки параметризован `Right/Left/Up/Down` — покрывает всю
  поверхность `ToKompas` на реальном COM (glm, kimi).
- Отклонено (обоснование): unit-тест `ToKompas` (граница проекта — тест не ссылается на
  `Kompas6Constants`); e2e-тест tool-обёртки (тонкая, `Parse`+сервис покрыты); `angleDeg`
  (у `ILeader`/`IBaseLeader` нет свойства Angle — ориентация геометрическая); выравнивание default
  `text` у `AddText` (вне объёма; required-контракт `AddLeader` корректнее).

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

- **Сигнатура** `DrawingValidation.RequireNonEmptyText(text, nameof(text))` (двухаргументный — как есть).
- **QI с диагностикой:** `bl as IBranchs ?? throw`, `bl as ILeader ?? throw` (не прямой cast); read-back
  `leader.TextOnShelf?.Str ?? string.Empty`; двухшаговый null-check `Leaders` (коллекция) и `Add()` —
  как `AddRoughCore`.
- **`ShelfDirections.ToKompas(Auto)` бросает `ArgumentOutOfRangeException`** (Auto обрабатывается в сервисе
  ДО вызова — «не задавать ShelfDirection»; прямой вызов для Auto — ошибка). RU-алиасы `Parse`:
  `авто/вправо/влево/вверх/вниз`.
- **Вырожденная выноска:** `RequireDistinctPoints(x, y, textX, textY)` (остриё ≠ полка; есть валидатор).
- **Критический порядок** (комментарий в коде): `AddBranchByPoint` ДО `SetBranchTextPosition`/`Update`
  (иначе RPC_E_SERVERFAULT — выноска без ветвей).
- **Тесты:** пустой текст — `""` И `"   "`; `viewNumber=0` резолвится в первый вид; путь `shelfDirection=auto`
  (ShelfDirection не задаётся). XML-`<summary>` членов `ShelfDirection` — «в ЛОКАЛЬНОЙ СК вида».
- Нейминг COM-объекта — `baseLeader` (не `bl`). `GetViewLeaderCountAsync` считает ВСЕ подтипы выносок
  (для v1 ок; фильтр — при добавлении ksDrPosLeader/… — в «Дальнейшее»).

## Цель

Веха 2D-ЧЕРТЁЖ, инкремент 9. Поставить на виде **линию-выноску** (leader) с текстовой надписью на полке —
поясняющее обозначение, указывающее стрелкой на элемент (примечание, материал детали, и т.п.).

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

Спайк (`_SpikeLeader`, прогнан на реальном КОМПАС, затем удалён). **Ключ:** только что созданная
выноска имеет **0 ответвлений** — любые операции над ответвлением (`SetSignPosition`) и `Update` на
выноске без ветвей падают с `RPC_E_SERVERFAULT`; сначала нужно добавить ответвление через `IBranchs`.

```
IBaseLeader bl = symbols.Leaders.Add(DrawingObjectTypeEnum.ksDrLeader);  // ksDrLeader=20
((IBranchs)bl).AddBranchByPoint(0, x, y);   // QI; ответвление 0 — остриё (куда указывает стрелка)
bl.SetBranchTextPosition(textX, textY);      // положение полки/текста
((ILeader)bl).TextOnShelf.Str = text;        // надпись на полке (.Str — наш interop)
// опц.: ((ILeader)bl).ShelfDirection = ksShelfDirectionEnum.ksLSRight;
bl.Update();   // True, bl.Valid == True
```

**Проверено:** `AddBranchByPoint(0,x,y)` → `BranchCount=1`; `Update=True`, `Valid=True`,
`TextOnShelf.Str` round-trip; `Leaders.Count` +1. Работает с явной `ShelfDirection` и без неё.
`BaseLeader` реализует `IBaseLeader`/`IDrawingObject`; `IBranchs` и `ILeader` — COM-QI от него.

Интерфейсы: `ILeaders.Add(DrawingObjectTypeEnum)` → `BaseLeader` (для обычной выноски `ksDrLeader=20`);
`IBranchs.AddBranchByPoint(int index, double x, double y)`; `IBaseLeader.SetBranchTextPosition(x,y)` /
`Update` / `Valid` / `Delete` / `ArrowType` (`ksArrowEnum`); `ILeader.TextOnShelf` (`IText`),
`ShelfDirection` (`ksShelfDirectionEnum`: `ksLSNone=0`/`ksLSRight=1`/`ksLSUp=2`/`ksLSDown=3`/`ksLSLeft=-1`).

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

| Инструмент | Параметры | Поведение |
|---|---|---|
| `drawing_add_leader` | `x,y` (остриё — куда указывает стрелка), `textX,textY` (полка/якорь текста), `text`, `shelfDirection="auto"` (auto\|right\|left\|up\|down), `viewNumber=0` | Поставить выноску на виде: стрелка из полки указывает в (x,y), текст `text` на полке у (textX,textY). Координаты — ЛОКАЛЬНАЯ СК вида (мм). `shelfDirection` — направление полки (auto = по умолчанию КОМПАС). Возвращает текст (read-back) и номер вида. |

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

В `DrawingService` (namespace `Kompas.Mcp.Core.Drawings`).
- `AddLeaderAsync(viewNumber, x, y, textX, textY, text, shelfDirection, ct)` → `DrawingAnnotationResult`
  (переиспользуем: `Value`=read-back текст, `ViewNumber`).
- Использует существующий `RequireSymbols2DContainer(viewNumber)` (выноски в `ISymbols2DContainer.Leaders`).
- Новый файл `src/Kompas.Mcp.Core/Drawings/ShelfDirection.cs`: `enum ShelfDirection {Auto,Right,Left,Up,Down}`
  (XML-`<summary>` на членах) + `ShelfDirections.Parse(string)` / `ToKompas(ShelfDirection)→ksShelfDirectionEnum`
  по образцу `AngleDimensionTypes`. `Auto` → не задавать `ShelfDirection` (оставить дефолт КОМПАС).
- Счётчик `GetViewLeaderCountAsync(viewNumber)` (для теста попадания в вид).

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

`AddLeaderCore`: `RequireNonEmptyText(text)` + `RequireFiniteCoords(x,y,textX,textY)` →
`RequireSymbols2DContainer` → `symbols.Leaders` (null-check) → `.Add(ksDrLeader)` (null-check) →
**внутри `try/catch`+`Delete`**: `((IBranchs)bl).AddBranchByPoint(0, x, y)` (QI null-check),
`bl.SetBranchTextPosition(textX, textY)`, `((ILeader)bl).TextOnShelf` (null-check) `.Str = text`,
если `shelfDirection != Auto`: `((ILeader)bl).ShelfDirection = ShelfDirections.ToKompas(...)` →
`bl.Update()` (FALSE → откат) → `bl.Valid` (false → откат) → read-back `((ILeader)bl).TextOnShelf.Str`
→ `DrawingAnnotationResult{Value=read-back, ViewNumber}`.

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

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

- `RequireNonEmptyText(text, nameof(text))`, `RequireFiniteCoords(x,y,textX,textY)`,
  `RequireDistinctPoints(x, y, textX, textY)` (остриё ≠ полка) — все уже есть.
- `ShelfDirections.Parse` — `auto/right/left/up/down` (+ рус. `авто/вправо/влево/вверх/вниз`, регистр/пробелы);
  неизвестное → `ArgumentException`; null → `ArgumentNullException`. `ToKompas` (→ `ksShelfDirectionEnum`)
  НЕ покрываем unit (граница проекта); бросает `ArgumentOutOfRangeException` на `Auto`.

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

### Unit (`ShelfDirectionsTests`)
- `Parse`: `auto/right/left/up/down` (+ регистр, рус.) → enum; неизвестное → `ArgumentException`; null → `ArgumentNullException`.

### Integration (`DrawingLeaderTests`; модель — коробка + стандартные виды)
1. **Ставится + round-trip + viewNumber=0**: `text="Примечание 1"`, `shelfDirection=auto`, `viewNumber=0`
   → `Value=="Примечание 1"` (read-back), `viewNumber=0` резолвится в первый вид (`r.ViewNumber` совпал),
   `Leaders.Count` целевого вида +1.
2. **shelfDirection=left** (не-auto путь) на целевом виде → ставится, `Leaders.Count` +1.
3. **Пустой текст** (`""` и `"   "`) → `ArgumentException` (до Add).
4. **Вырожденная** (остриё совпадает с полкой) → `ArgumentException` (до Add).
5. **Нет видов** → понятная ошибка. 6. **Несуществующий `viewNumber>0`** → понятная ошибка.

## Дальнейшее (вне спека)
- Многострочный текст полки / текст под полкой (`TextUnderShelf`); несколько ответвлений
  (`AddBranchByPoint` с разными индексами); привязка к геометрии (`IBranchs.SetBaseObject`); тип стрелки
  (`ArrowType`); специальные выноски (позиция/клеймо/маркер: `ksDrPosLeader/ksDrBrandLeader/ksDrMarkerLeader`).
  Базы (`Bases`), допуски формы (`Tolerances`).
