feat(feature): операция Ребро жёсткости (rib)

RibAsync (o3d_ribOperation): SetSketch + index/angle/side + SetThinParam.
SketchGeometry.RibSide маппинг (left/right/up/down). Инструмент rib в FeatureTools.
Эмпирически: контур ребра висит в зазоре (не касается тела), side=left для косынки.
Integration: L-уголок + ребро в углу → объём растёт.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 22:51:58 +03:00
parent 7a9964b6f1
commit fc99739e1e
6 changed files with 171 additions and 0 deletions
@@ -0,0 +1,70 @@
# Дизайн: пакет B (шаг 2) — операция «Ребро жёсткости» (rib)
**Дата:** 2026-05-26
**Статус:** на ревью (автономный режим; ревьюер — Codex)
## Цель
Добавить формообразующую операцию **«Ребро жёсткости»** (rib) — достроить тонкую стенку от
разомкнутого контура эскиза до тела. Второй шаг пакета B. Один MCP-инструмент `rib`.
## Сигнатуры COM (рефлексия interop + SDK-база + ksConstants3D.h)
```
Obj3dType.o3d_ribOperation = 44
ksRibDefinition:
int index // сегмент эскиза, задающий уклон (0 — первый сегмент)
double angle // угол уклона стенок, градусы (0 — вертикальные)
int side // направление достройки: ksRibSideEnum
bool SetSketch(object) / GetSketch()
bool SetThinParam(short thinType, double normalThickness, double reverseThickness)
ksRibSideEnum: ksRibSideLeft=0, ksRibSideRight=1, ksRibSideUp=2, ksRibSideDown=3
Direction_Type (thinType): dtNormal=0, dtReverse=1, dtBoth=2, dtMiddlePlane=3
```
**Паттерн:** `NewEntity(o3d_ribOperation)``GetDefinition()``SetSketch`
`index`/`angle`/`side`/`SetThinParam``Create()`. Порядок: сначала `SetSketch`, затем
параметры, затем `Create()`.
**Эскиз:** разомкнутый контур (отрезок/ломаная) на плоскости, пересекающей тело; КОМПАС
сам «дотягивает» ребро до тела.
## Новый MCP-инструмент (группа Feature)
| Инструмент | Параметры | Поведение |
|---|---|---|
| `rib` | `sketchId: int`, `thickness: double`, `side: string = "up"`, `symmetric: bool = true`, `angle: double = 0` | Построить ребро жёсткости от разомкнутого контура эскиза `sketchId` до тела. `thickness` — толщина стенки (мм). `side` ∈ {left,right,up,down} — направление достройки. `symmetric=true` — толщина симметрична плоскости эскиза; `false` — в одну сторону. `angle` — уклон стенок (°). |
**Маппинги:**
- `side`: `left→0, right→1, up→2, down→3` (через `SketchGeometry.RibSide(string)`, unit-тест).
- `symmetric=true → SetThinParam(dtBoth=2, thickness/2, thickness/2)`;
`false → SetThinParam(dtNormal=0, thickness, 0)`.
- `index = 0` (фиксировано — первый сегмент контура).
## Реализация
- **Метод** `RibAsync(int sketchId, double thickness, string side, bool symmetric, double angle, CancellationToken)`
в `PartModeler.Features.cs`. Эскиз закрывается перед операцией (`CloseSketchCore`, как
`ExtrudeAsync`/`RevolveAsync`); берётся `sketch.Entity` из реестра по id. Возвращает id операции.
- **Маппинг side** — `SketchGeometry.RibSide(string)` (public static, unit-тест; бросает
`ArgumentException` на неизвестное значение).
- **Инструмент** `rib` в `FeatureTools.cs`.
- **Валидация** (inline): `thickness > 0`; `side` — известное значение (через `RibSide`).
`Create()==FALSE``InvalidOperationException` (контур не пересекает тело / некорректное
направление?).
- Транзитный `ksRibDefinition` не освобождается — консистентно с прочими операциями (долг v2-2).
## Тестирование
**Unit:** `SketchGeometry.RibSide` — маппинг строк в коды и бросок на неизвестном значении.
**Integration** (`FeatureOpsTests`): Г-образный уголок (L-профиль на XOZ, замкнутая ломаная
`(0,0)(40,0)(40,5)(5,5)(5,40)(0,40)`, extrude по Y на 30 → объём 375·30=11250) → эскиз ребра
на XOZ: разомкнутый отрезок-диагональ во внутреннем углу (например `(25,10)→(10,25)`, **висит в зазоре, не касаясь полок**) →
`RibAsync(sketchId, thickness=5, side="left", symmetric=false)` → объём увеличивается
(добавлена косынка): `after > before`, прирост правдоподобный (`after ∈ (before, before·1.3)`).
**Проверено эмпирически (важно для контракта):** контур ребра должен **висеть в зазоре**
между полками (не касаться граней тела концами — иначе `Create()==FALSE`); КОМПАС сам
дотягивает полотно до тела в направлении `side`. Для косынки во внутреннем углу сработало
`side="left"`, `symmetric=false` (толщина в прямом направлении нормали плоскости).