Files
kompas3d-mcp/docs/superpowers/plans/2026-05-26-shell.md
T
mikhail 115fcb8759 feat(feature): операция Оболочка (shell)
ShellAsync (o3d_shellOperation): удаление граней по индексам + толщина стенки,
thinType=!outward. SelectFaceByIndex в ядре, инструмент shell в FeatureTools.
Валидация: thickness>0, дедупликация индексов, проверка диапазона до мутации.
Integration-тест: коробка 40x30x20 → оболочка 2мм → 7152 мм³ (±5%).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 22:28:03 +03:00

171 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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;
/// <summary>Интеграция: формообразующие операции пакета B.</summary>
[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
/// <summary>Выбрать грань детали по индексу из list_faces (с проверкой диапазона).</summary>
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
/// <summary>Превратить тело в оболочку: удалить (открыть) грани с индексами faceIndices,
/// задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется),
/// true — наружу. Возвращает id операции.</summary>
public Task<int> ShellAsync(IReadOnlyList<int> 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<string> 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`.