# Дизайн: пакет B (шаг 1) — операция «Оболочка» (shell)

**Дата:** 2026-05-26
**Статус:** на ревью (автономный режим; ревьюер — Codex)
**Контекст:** kompas3d-mcp — MCP-сервер для КОМПАС-3D через COM API (.NET 8, C#).

## Цель

Добавить формообразующую операцию **«Оболочка»** (придать стенкам толщину, удалив выбранные
грани) — первый шаг пакета B «формообразующие». Самая востребованная операция этого пакета;
опирается на уже готовый выбор грани по индексу из `list_faces`, без мульти-эскизной
оркестрации (нужной для loft/sweep). Один MCP-инструмент `shell`.

## Не входит в объём (YAGNI)

- Остальные операции пакета B (rib, draft, loft, sweep, hole) — отдельные шаги.
- Переменная толщина по граням (`ksShellDefinition` задаёт одну общую `thickness`).
- Закрытая оболочка без удаляемых граней — КОМПАС требует ≥1 удаляемую грань.

## Сигнатуры COM (подтверждены: рефлексия interop + SDK-база + пример Step3d1.cs)

```
Obj3dType.o3d_shellOperation = 43
ksShellDefinition:
    double  thickness         // толщина стенки, мм
    bool    thinType          // true = внутрь (габарит сохраняется), false = наружу
    object  FaceArray()       // ksEntityCollection удаляемых (открываемых) граней; нужна >=1
```

**Паттерн** (из `Samples/CSharp.zip → Step3d1.cs`):
```
ksEntity ent = part.NewEntity(o3d_shellOperation)
ksShellDefinition def = ent.GetDefinition()
ksEntityCollection faces = def.FaceArray()
faces.Add(<грань>)                 // одна или несколько удаляемых граней
def.thickness = t; def.thinType = !outward
ent.Create()
```
Порядок: сначала заполнить `FaceArray`, затем `thickness`/`thinType`, затем `Create()`.

## Новый MCP-инструмент (группа Feature)

| Инструмент | Параметры | Поведение |
|---|---|---|
| `shell` | `faceIndices: int[]`, `thickness: double`, `outward: bool = false` | Превратить тело в оболочку толщиной `thickness` мм, удалив (открыв) грани с указанными индексами из `list_faces`. `outward=false` — толщина внутрь (габарит сохраняется), `true` — наружу. |

**Маппинг:** `thinType = !outward`.

**Выбор граней:** по стабильному индексу из `list_faces` (как `sketch_create_on_face_index`,
`fillet_edge_index`). Индексы валидны, пока геометрия не менялась — берутся из свежего
`list_faces`. Несколько граней → несколько `FaceArray.Add`.

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

- **Метод модели** `ShellAsync(IReadOnlyList<int> faceIndices, double thickness, bool outward, CancellationToken)`
  в `PartModeler.Features.cs` (формообразующая). Возвращает id операции (как extrude/revolve).
- **Helper** `SelectFaceByIndex(ksPart, int)` в ядре `PartModeler.cs` — аналог существующего
  `SelectEdgeByIndex` (диапазон-проверка + `EntityCollection(o3d_face).GetByIndex`).
- **Инструмент** `shell` в `FeatureTools.cs` (как `extrude_boss`/`revolve_boss`).
- **Валидация** (inline, как `ExtrudeAsync`/`RevolveAsync`): `thickness > 0`; `faceIndices`
  не null и не пуст; **каждый индекс проверяется на диапазон до мутации модели**
  (`SelectFaceByIndex` бросает `ArgumentOutOfRangeException`, как `SelectEdgeByIndex`);
  **дубли индексов отбрасываются** (`Distinct`), чтобы не добавлять грань в `FaceArray` дважды;
  `FaceArray()` проверяется на null (`as ksEntityCollection ?? throw`). Возврат
  `Create()==FALSE` → `InvalidOperationException` с пояснением вероятных причин (толщина больше
  локального радиуса/толщины стенки; несовместимая топология).
- **Известное ограничение** (документируем, не обрабатываем): на телах со скруглениями/фасками и
  на вогнутых/криволинейных гранях оболочка может не построиться (`Create()==FALSE`) даже при
  формально корректных входных данных — это ограничение ядра КОМПАС, причину API не сообщает.

## Тестирование

**Integration** (`Category=Integration`, `KompasFixture`, в `SketchPrimitivesTests` или новом
`FeatureOpsTests`): коробка 40×30×20 (`AddRectangle` → `Extrude`) → `list_faces` → найти
верхнюю грань (нормаль +Z или по площади) → `ShellAsync([верхняя], thickness=2, outward=false)`
→ объём существенно уменьшается (тело стало полым с открытым верхом) и остаётся > 0.
Точная проверка: оболочка 2 мм с открытым верхом = `24000 − 36×26×18 = 24000 − 16848 = 7152 мм³`.
Утверждение: `after < before` и `after ∈ (7152·0.95, 7152·1.05)` (≈6794…7510 мм³). Если КОМПАС
считает полость иначе — скорректировать ожидание по фактическому замеру, сохранив узкий допуск.

Unit отдельный не вводим: маппинг `thinType=!outward` тривиален и инлайнится; валидация
параметров — в стиле существующих операций (`extrude`/`revolve` тоже покрыты интеграционно).

## Влияние на документацию

После реализации (через `docs-delegate`): счётчик инструментов +1 (→51), тестов +1;
добавить `shell` в описание формообразующих в `CLAUDE.md`/`README.md`/`docs/ARCHITECTURE.md`/
`docs/presentation.html`; отметить начало пакета B.
