Files
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

9.0 KiB
Raw Permalink Blame History

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

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)
/// <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
/// <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)
[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
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.