# Shell (операция «Оболочка») Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps use checkbox (`- [ ]`). **Goal:** MCP-инструмент `shell` — превратить тело в оболочку заданной толщины, удалив выбранные грани. **Architecture:** `ShellAsync` в `PartModeler.Features.cs` (паттерн `NewEntity(o3d_shellOperation)→GetDefinition→FaceArray.Add→thickness/thinType→Create`), helper `SelectFaceByIndex` в ядре, инструмент `shell` в `FeatureTools.cs`. Грани — по индексам из `list_faces`. **Tech Stack:** .NET 8 x64, C#, COM API5 (`Kompas6API5.ksShellDefinition`, `Kompas6Constants3D.Obj3dType.o3d_shellOperation`=43). --- ## Task 1: shell end-to-end **Files:** - Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.cs` (helper) - Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs` (метод) - Modify: `src/Kompas.Mcp.Host/Tools/FeatureTools.cs` (инструмент) - Test: `tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs` - [ ] **Step 1: Падающий интеграционный тест** `FeatureOpsTests.Shell_box_hollows_with_open_top` ```csharp using Kompas.Mcp.Core.Documents; using Kompas.Mcp.Core.Modeling; using Kompas.Mcp.Core.Query; namespace Kompas.Mcp.Tests.Integration; /// Интеграция: формообразующие операции пакета B. [Trait("Category", "Integration")] [Collection(KompasCollection.Name)] public sealed class FeatureOpsTests { private readonly DocumentService _docs; private readonly PartModeler _modeler; private readonly QueryService _query; public FeatureOpsTests(KompasFixture fx) { _docs = new DocumentService(fx.Session, fx.Dispatcher); _modeler = new PartModeler(fx.Session, fx.Dispatcher); _query = new QueryService(fx.Session, fx.Dispatcher); } [Fact] public async Task Shell_box_hollows_with_open_top() { await _docs.CreateAsync(KompasDocumentType.Part); try { // Коробка 40×30×20. var s = await _modeler.OpenSketchAsync(BasePlane.XOY); await _modeler.AddRectangleAsync(s, 0, 0, 40, 30); await _modeler.CloseSketchAsync(s); await _modeler.ExtrudeAsync(s, depth: 20); var before = (await _query.GetPartInfoAsync()).Volume; // 24000 // Удаляем одну из двух плоских граней-торцов (площадь 40×30=1200) → открытая оболочка. var faces = await _query.ListFacesAsync(); var cap = faces.First(f => f.Type == "plane" && Math.Abs(f.Area - 1200) < 1); var id = await _modeler.ShellAsync(new[] { cap.Index }, thickness: 2, outward: false); Assert.True(id > 0); await _modeler.RebuildAsync(); // Оболочка 2 мм с открытым торцом: 24000 − 36×26×18 = 7152 мм³. var after = (await _query.GetPartInfoAsync()).Volume; Assert.True(after < before); Assert.InRange(after, 7152 * 0.95, 7152 * 1.05); } finally { await _docs.CloseAsync(save: false); } } } ``` - [ ] **Step 2: Запустить — убедиться, что падает** Run: `dotnet test -c Release --filter "FullyQualifiedName~FeatureOpsTests.Shell_box_hollows_with_open_top"` Expected: FAIL (метод `ShellAsync` не существует). - [ ] **Step 3: Helper `SelectFaceByIndex`** в `PartModeler.cs` (рядом с `SelectEdgeByIndex`) ```csharp /// Выбрать грань детали по индексу из list_faces (с проверкой диапазона). private static ksEntity SelectFaceByIndex(ksPart part, int faceIndex) { var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection ?? throw new InvalidOperationException("Не удалось получить коллекцию граней."); if (faceIndex < 0 || faceIndex >= faces.GetCount()) throw new ArgumentOutOfRangeException(nameof(faceIndex), $"Индекс грани вне диапазона [0; {faces.GetCount() - 1}]. Сверьтесь с list_faces."); return faces.GetByIndex(faceIndex) as ksEntity ?? throw new InvalidOperationException("Грань по индексу не приводится к ksEntity."); } ``` - [ ] **Step 4: Метод `ShellAsync`** в `PartModeler.Features.cs` ```csharp /// Превратить тело в оболочку: удалить (открыть) грани с индексами faceIndices, /// задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется), /// true — наружу. Возвращает id операции. public Task ShellAsync(IReadOnlyList faceIndices, double thickness, bool outward = false, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { if (thickness <= 0) throw new ArgumentOutOfRangeException(nameof(thickness), "Толщина должна быть > 0."); ArgumentNullException.ThrowIfNull(faceIndices); var indices = faceIndices.Distinct().ToList(); if (indices.Count == 0) throw new ArgumentException("Нужна хотя бы одна удаляемая грань.", nameof(faceIndices)); var part = GetTopPart(); // Проверяем диапазон ВСЕХ индексов до мутации модели. var faces = indices.Select(i => SelectFaceByIndex(part, i)).ToList(); var entity = part.NewEntity((short)Obj3dType.o3d_shellOperation) as ksEntity ?? throw new InvalidOperationException("NewEntity(o3d_shellOperation) вернул null."); var def = entity.GetDefinition() as ksShellDefinition ?? throw new InvalidOperationException("GetDefinition() оболочки вернул не ksShellDefinition."); var arr = def.FaceArray() as ksEntityCollection ?? throw new InvalidOperationException("FaceArray() оболочки вернул не ksEntityCollection."); foreach (var f in faces) arr.Add(f); def.thickness = thickness; def.thinType = !outward; // true=внутрь, false=наружу if (!entity.Create()) throw new InvalidOperationException( "Create() оболочки вернул FALSE (толщина больше локального радиуса/стенки или несовместимая топология?)."); var id = _nextId++; _features[id] = entity; return id; }, ct); ``` - [ ] **Step 5: Инструмент `shell`** в `FeatureTools.cs` (перед `Rebuild`) ```csharp [McpServerTool(Name = "shell")] [Description("Превратить тело активной детали в оболочку (придать стенкам толщину): удалить (открыть) грани с индексами faceIndices из list_faces и задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется), true — наружу. Возвращает id операции.")] public async Task Shell( [Description("Индексы удаляемых (открываемых) граней из list_faces")] int[] faceIndices, [Description("Толщина стенки, мм")] double thickness, [Description("Толщина наружу (true) или внутрь (false)")] bool outward = false) { await session.ConnectAsync(); var id = await modeler.ShellAsync(faceIndices, thickness, outward); return $"Оболочка толщиной {thickness} мм создана (удалено граней: {faceIndices.Length}), id={id}."; } ``` - [ ] **Step 6: Запустить тест** Run: `dotnet test -c Release --filter "FullyQualifiedName~FeatureOpsTests.Shell_box_hollows_with_open_top"` Expected: PASS (если фактический объём иной — скорректировать ожидание по замеру, сохранив узкий допуск). - [ ] **Step 7: Commit** ```bash git add src/Kompas.Mcp.Core/Modeling/PartModeler.cs src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs src/Kompas.Mcp.Host/Tools/FeatureTools.cs tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs git commit -m "feat(feature): операция Оболочка (shell)" ``` --- ## Task 2: документация и завершение - [ ] Полный прогон: `dotnet test -c Release --filter "Category=Unit"` и `--filter "Category=Integration"` — всё зелёное. - [ ] Codex-ревью реализации; внести обоснованные правки. - [ ] `docs-delegate`: +1 инструмент (→51), +1 тест; `shell` в описание формообразующих; начат пакет B. - [ ] Merge в `main`.