# Дизайн: пакет 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` (толщина в прямом направлении нормали плоскости).
