# Дизайн: отверстие (hole) через API7

**Дата:** 2026-05-27
**Статус:** реализовано и проверено (spike зелёный); на ревью (ревьюер — Codex)

## Цель

Добавить параметрическую операцию **«Отверстие»** (простое цилиндрическое, ksHTBase) — сквозное
или глухое, размещённое на грани по мировой точке. Приоритет 2 из плана.

## Ключевой факт: отверстие — только API7

В **API5 определения отверстия НЕТ** (по рефлексии `Kompas6API5.dll` нет ни одного типа с `Hole`;
есть лишь `Obj3dType.o3d_holeOperation=52` без интерфейса — как было с draft). В **API7** —
богатая поддержка: `IModelContainer.Holes3D` (`IHoles3D`) → `Add()` → `IHole3D`, размещение через
`IHoleDisposal`. Прецедент API7 в слое моделирования уже есть — `move_face` (`FaceEditService`).
Поэтому отдельный сервис `HoleService` (API7), а не метод `PartModeler` (API5).

## Подтверждённый workflow (spike зелёный)

```
IPart7 top = (IPart7)doc3d.TopPart;
IFace face = top.FindObjectsByPoint(x,y,z,true) → первый IFace;   // как в move_face
IModelContainer c = (IModelContainer)top;                          // COM-QI

// Точка размещения центра отверстия в МИРОВЫХ координатах:
IPoint3D pt = c.Points3D.Add();
pt.ParameterType = ksPParamCoord; pt.X=x; pt.Y=y; pt.Z=z; pt.Update();

IHole3D hole = c.Holes3D.Add();
hole.HoleType    = ksHTBase;                                       // простое цилиндрическое
hole.Diameter    = diameter;                                       // мм
hole.DepthType   = throughAll ? ksDTReachThrough : ksDTValue;
if (!throughAll) hole.Depth = depth;                               // мм
hole.EndFaceType = ksEFFlat;  hole.Axis=false;  hole.ShowThread=false;

IHoleDisposal d = (IHoleDisposal)hole;                             // QI
d.BaseSurface = (IModelObject)face;                                // обязательна грань
d.Perpendicular = true;
d.AssociationVertex = (IModelObject)pt;                            // центр = созданная точка
d.Direction = true;                                                // подбор по факту (см. ниже)
hole.Update(); doc3d.RebuildDocument();                            // проверяем убыль объёма; иначе Direction=false
```

**Направление сверления (важно):** нормаль грани ориентирована непредсказуемо (тот же класс
проблем, что у boss/cut в OPEN_QUESTIONS), а при **сквозном** отверстии `Update()` возвращает
`TRUE` даже когда сверлит «в воздух» (наружу тела → удалено 0 материала). Поэтому направление
подбираем **по факту удаления материала**: строим с `Direction=true`, перестраиваем, сравниваем
объём (API5 `CalcMassInertiaProperties`, согласован с API7-геометрией после `RebuildDocument`);
если объём не убыл — `Direction=false` и повтор; если оба не убавили — ошибка. Только успех
Update недостаточен.

**Размещение** (главная неочевидность, решена): у `IHole3D` нет свойства позиции — оно в
`IHoleDisposal` (`BaseSurface` = грань + `AssociationVertex` = точка/вершина). Самый надёжный
способ задать центр по мировым координатам — создать `IPoint3D` (`ksPParamCoord` + X/Y/Z) и
передать его как `AssociationVertex`. Эскиз размещения НЕ требуется (в отличие от `extrude_cut`).

Единицы: `Diameter`/`Depth`/`X`/`Y`/`Z` — мм. `EndFaceType=ksEFFlat` (плоский торец) для глухого.

## MCP-инструмент (группа Feature)

| Инструмент | Параметры | Поведение |
|---|---|---|
| `hole` | `x,y,z: double`, `diameter: double`, `depth=0`, `throughAll=false` | Просверлить цилиндрическое отверстие на грани, найденной по точке (x,y,z — центр). `throughAll` — сквозное (depth не нужен), иначе глухое на `depth`. Направление в тело — авто (как move_face). |

**Решения (YAGNI):** на первом этапе только `ksHTBase` (простое цилиндрическое); зенковка/
цековка/коническое (`ksHTCounterbore/Countersinking/Conic` + параметрические подынтерфейсы) и
резьба (`ShowThread`/`IThread`) — расширения позже. Глубина «до объекта» (`ksDTObject`) не
выставляется. Отверстие не регистрируется в реестре `_features` (он для API5-операций) — массивы/
зеркало пакета C к нему неприменимы; контракт инструмента возвращает подтверждение без id.

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

- **`src/Kompas.Mcp.Core/Modeling/HoleService.cs`** (новый): `HoleAsync(x,y,z,diameter,depth,throughAll,ct)`
  на STA-потоке; валидация `double.IsFinite` для координат/диаметра/глубины + `>0`; `FindFaceAtPoint`
  (копия паттерна из `FaceEditService`); подбор направления по убыли объёма (`TryDirection`);
  при неуспехе — откат точки и объекта отверстия (`IFeature7.Delete`), затем `RebuildDocument`.
- **DI:** `AddSingleton<HoleService>()` в `Program.cs`.
- **Инструмент** `hole` в `FeatureTools.cs` (конструктор получает `HoleService`).
- Транзитные RCW не освобождаем точечно — консистентно (долг v2-2).

## Тестирование (Integration, `HoleTests`)

1. **Сквозное**: коробка 40×40×20 (центр 0,0; верх Z=20) → `hole(0,0,20, Ø10, throughAll)` →
   удалён цилиндр π·25·20 ≈ 1570.8 мм³; `InRange(before-after, ±5%)`. ✓
2. **Глухое**: та же коробка → `hole(0,0,20, Ø8, depth=10)` → удалён π·16·10 ≈ 502.7 мм³;
   `InRange(before-after, ±10%)` (торец плоский, погрешность модели). ✓
3. **Нижняя грань (подбор направления)**: та же коробка → `hole(0,0,0, Ø10, throughAll)` со стороны
   нижней грани → корректное направление внутрь (+Z) подбирается по убыли объёма; удалён π·25·20. ✓
