Files
kompas3d-mcp/docs/superpowers/specs/2026-05-27-hole-design.md
T
mikhail d3dd78ec0b fix: правки hole по ревью Codex
- направление сверления — по факту убыли объёма (Update=TRUE даже при сверлении в воздух
  на сквозном отверстии); TryDirection + CurrentVolume (API5 МЦХ) — P2-баг, вскрыт тестом
- валидация double.IsFinite для координат/диаметра/глубины (Infinity проходил мимо >0) — P2
- откат точки-сироты и объекта отверстия при неуспехе (IFeature7.Delete) + RebuildDocument — P2
- тест Hole_through_bottom_face_uses_direction_fallback (нижняя грань) — P3
- 104 теста зелёных
2026-05-27 10:04:22 +03:00

7.2 KiB
Raw Blame History

Дизайн: отверстие (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. ✓