feat: уклон граней (draft) через API5 — ksInclineDefinition o3d_incline=42

- ВОПРЕКИ прежнему предположению: уклон ЕСТЬ в API5 (операция Incline, не Draft) →
  стандартный паттерн PartModeler, регистрируется в _features (массивы/зеркало применимы)
- DraftAsync: FaceArray().Add(грани) + SetPlane(нейтральная коорд. плоскость) + angle + direction
- ЭМПИРИЧЕСКИ: direction=false=расширение, true=сужение (обратно справке) → маппинг !outward
- инструмент draft (faceIndices, neutralPlane, angle 0..90, outward); валидация угла и индексов
- интеграционный тест: 4 боковые грани коробки → усечённая пирамида (объём ±10%)
- 105 тестов; спек docs/superpowers/specs/2026-05-27-draft-design.md
This commit is contained in:
2026-05-27 10:23:16 +03:00
parent 4f7e8f3b7e
commit 205a4b02de
4 changed files with 153 additions and 0 deletions
@@ -0,0 +1,67 @@
# Дизайн: уклон граней (draft / incline) через API5
**Дата:** 2026-05-27
**Статус:** реализовано и проверено (тест зелёный); на ревью (ревьюер — Codex)
## Цель
Добавить операцию **«Уклон»** (draft) — наклонить грани на угол относительно нейтральной
(опорной) плоскости. Приоритет 1 текущей сессии.
## Ключевой факт: уклон ЕСТЬ в API5 (вопреки прежнему предположению)
Память `kompas-formative-features-api5` ошибочно считала draft недоступным в API5 — она искала
тип со словом **Draft**, но операция называется **«Уклон» = Incline**. Рефлексия подтвердила:
`Obj3dType.o3d_incline=42` + **`ksInclineDefinition`** есть в `Kompas6API5.dll`. Это стандартный
API5-паттерн (как shell/rib/fillet) — НЕ нужен API7. Плюс: операция регистрируется в реестре
`_features`, значит массивы/зеркало пакета C к ней применимы.
> `o3d_DraftFromEdges=644` (API7 `IDraftFromEdges`) — это ДРУГАЯ операция «уклон от базовой линии»
> (v22), без нейтральной плоскости. Для обычного уклона граней используем `IIncline`/`ksInclineDefinition`.
## Сигнатуры (рефлексия interop + справка)
```
Obj3dType.o3d_incline = 42
ksInclineDefinition:
double angle // угол уклона (градусы)
bool direction // ЭМПИРИЧЕСКИ: false=расширение, true=сужение (обратно справке!)
object FaceArray() // ksEntityCollection уклоняемых граней (Add по одной)
bool SetPlane(object) // нейтральная (опорная) плоскость — ksEntity
object GetPlane()
```
**Паттерн:** `NewEntity(o3d_incline)``GetDefinition()``FaceArray().Add(face)` (по граням) →
`SetPlane(нейтральная)``angle``direction``Create()` → регистрация в `_features`.
**Нейтральная плоскость** — сечение тела в ней остаётся неизменным; грани уклоняются вокруг
линии пересечения с ней. На первом этапе — базовая координатная плоскость (XOY/XOZ/YOZ).
**Направление (важно):** в interop `direction=false` даёт **расширение**, `true`**сужение**
(проверено объёмом; обратно тексту справки). Контракт инструмента: `outward` (true=расширение),
маппинг `def.direction = !outward`.
## MCP-инструмент (группа Feature)
| Инструмент | Параметры | Поведение |
|---|---|---|
| `draft` | `faceIndices: int[]`, `neutralPlane: "XOY"\|"XOZ"\|"YOZ"`, `angle: double`, `outward=false` | Уклон граней `faceIndices` (из list_faces) на `angle`° относительно нейтральной координатной плоскости. `outward` — расширение/сужение. Возвращает id. |
**Решения (YAGNI):** нейтральная плоскость — только координатная (как mirror_operation); уклон от
ребра (`IDraftFromEdges`) — отдельная задача позже. Грани — по стабильному индексу из list_faces.
## Реализация
- **Метод** `DraftAsync(IReadOnlyList<int> faceIndices, BasePlane neutralPlane, double angle, bool outward, ct)`
в `PartModeler.Features.cs`. Валидация: `angle` конечен и в (0; 90); `faceIndices` не пуст,
`Distinct`; диапазон всех индексов проверяется до мутации (`SelectFaceByIndex`).
- **Инструмент** `draft` в `FeatureTools.cs` (`neutralPlane` парсится `BasePlanes.Parse`).
- Транзитные RCW не освобождаем точечно — консистентно (долг v2-2).
- `Create()==FALSE``InvalidOperationException` (грань не граничит с нейтральной плоскостью?).
## Тестирование (Integration, `FeatureOpsTests`)
**Уклон**: коробка 40×40×20 (низ на Z=0) → 4 боковые грани (площадь 800) под уклон 10° внутрь
(`outward=false`) относительно XOY → усечённая пирамида: верх ≈ 402·20·tan10°=32.95;
`V = (20/3)(1600+1085.5+√(1600·1085.5)) ≈ 26689` мм³; `InRange(after, ±10%)` и `after < before`. ✓
Боковые грани выбираются из `ListFaces` по площади 800 (тип plane).