Files
kompas3d-mcp/docs/superpowers/plans/2026-05-26-sketch-primitives.md
T
mikhail dc4003810d feat(sketch): 7 примитивов эскиза — дуга x2, эллипс, ломаная, многоугольник, сплайн, точка
Методы PartModeler + MCP-инструменты sketch_add_arc_3points/arc/ellipse/
polyline/polygon/spline/point. NURBS порядок 4, ksEndObj через try/finally,
param-структуры через NewParam с освобождением RCW. 7 интеграционных
тестов (профиль → выдавливание → объём) зелёные.

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

46 KiB
Raw Blame History

Sketch Primitives (пакет A) — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Добавить 7 MCP-инструментов эскиза (дуга ×2, эллипс, ломаная, правильный многоугольник, сплайн, точка) поверх существующего цикла «эскиз → операция».

Architecture: Новые методы — тонкие обёртки над ksDocument2D (API5) в открытом эскизе, по образцу существующих AddLineAsync/AddCircleAsync. ellipse/polygon/spline используют параметрические структуры, получаемые через KompasObject.GetParamStruct(...) и освобождаемые в finally. PartModeler разбивается на partial-файлы (ядро/эскизы/операции). Чистые маппинги и валидация вынесены в SketchGeometry для unit-тестов; геометрия проверяется интеграционно (профиль → extrude → объём).

Tech Stack: .NET 8 (net8.0-windows, x64), C#, COM API5 (Kompas6API5, Kompas6Constants), MCP SDK ModelContextProtocol, xUnit.

Подтверждено рефлексией по libs/kompas-interop/ (сигнатуры — дословно):

Int32 ksArcBy3Points(Double x1, y1, x2, y2, x3, y3, Int32 style)
Int32 ksArcByAngle(Double xc, yc, rad, f1, f2, Int16 direction, Int32 style)   // углы в градусах
Int32 ksEllipse(Object par)              // par = ksEllipseParam { xc, yc, A, B, angle(град), style }
Int32 ksRegularPolygon(Object par, Int16 centre)  // par = ksRegularPolygonParam { count, xc, yc, ang(град), radius, describe, style }
Int32 ksNurbs(Int16 degree, Boolean close, Int32 style)
Int32 ksNurbsPoint(Object par)           // par = ksNurbsPointParam { x, y, weight }
Int32 ksEndObj()
Int32 ksPoint(Double x, y, Int32 style)

Коды структур (Kompas6Constants.StructType2DEnum): ko_EllipseParam=22, ko_RegularPolygonParam=92, ko_NurbsPointParam=18. Стиль линии «основная» = 1; стиль точки = 0.

Источники истины (во избежание расхождений): Automation-семантику методов берём из docs/Kompas3D_SDK/; точные C#-имена свойств и типы — из рефлексии по libs/kompas-interop/ (interop генерирует, например, ksEllipseParam.A/.B с заглавной — это канон для компиляции, регистр в C# важен). При конфликте имени побеждает interop.

NURBS — порядок, не степень: параметр degree метода ksNurbs — это порядок кривой (степень полинома + 1, диапазон 3..10). Кубический сплайн = порядок 4. В плане используем константу SplineOrder = 4.

Регистрация: доп. DI не нужна — PartModeler уже синглтон в Program.cs, инструменты подхватываются WithToolsFromAssembly().

Команды:

  • Сборка: dotnet build -c Release
  • Unit: dotnet test --filter "Category=Unit"
  • Integration (нужен запущенный КОМПАС): dotnet test --filter "Category=Integration"

Task 1: Рефакторинг PartModeler в partial class

Чистое перемещение существующих методов по трём файлам — поведение не меняется. Регрессия = существующие тесты зелёные.

Files:

  • Modify: src/Kompas.Mcp.Core/Modeling/PartModeler.cs

  • Create: src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs

  • Create: src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs

  • Step 1: Создать PartModeler.Sketch.cs с этим заголовком и перенести в него (вырезать из PartModeler.cs) методы: OpenSketchAsync, OpenSketchOnFaceAsync, OpenSketchOnFaceIndexAsync, CreateSketchOn, AddLineAsync, AddCircleAsync, AddRectangleAsync, AddAxisAsync, CloseSketchAsync (тела без изменений).

using System.Runtime.Versioning;
using Kompas.Mcp.Core.Interop;
using Kompas6API5;
using Kompas6Constants3D;

namespace Kompas.Mcp.Core.Modeling;

/// <summary>PartModeler: создание эскизов и добавление 2D-примитивов.</summary>
[SupportedOSPlatform("windows")]
public sealed partial class PartModeler
{
    // (перенесённые методы эскизов)
}
  • Step 2: Создать PartModeler.Features.cs с этим заголовком и перенести в него (вырезать из PartModeler.cs) методы: ExtrudeAsync, RevolveAsync, FilletEdgeAsync, FilletEdgeIndexAsync, CreateFillet, ChamferEdgeAsync, ChamferEdgeIndexAsync, CreateChamfer, RebuildAsync (тела без изменений).
using System.Runtime.Versioning;
using Kompas6API5;
using Kompas6Constants3D;

namespace Kompas.Mcp.Core.Modeling;

/// <summary>PartModeler: формообразующие операции (выдавливание, вращение, скругление, фаска).</summary>
[SupportedOSPlatform("windows")]
public sealed partial class PartModeler
{
    // (перенесённые методы операций)
}
  • Step 3: В PartModeler.cs изменить объявление класса на public sealed partial class PartModeler : IDisposable и оставить только ядро: поля (_session, _dispatcher, _nextId, _sketches, _features), константы (MainLineStyle, AxisLineStyle), ctor, вложенный класс SketchEntry, helpers ActiveDoc3D, GetTopPart, SelectEdgeByPoint, SelectEdgeByIndex, RequireSketch, RequireOpenSketch, CloseSketchCore, ResetCore, ReleaseCom, ResetAsync, Dispose. Добавить общий helper для параметрических структур (используется в Task 5/7/8):
/// <summary>Создать параметрическую 2D-структуру через KompasObject (API5) и привести к интерфейсу.
/// При неудачном приведении освобождает сырой RCW, чтобы не утекал COM-объект.</summary>
private T NewParam<T>(Kompas6Constants.StructType2DEnum kind) where T : class
{
    var raw = _session.Kompas.GetParamStruct((short)kind);
    if (raw is T typed) return typed;
    ReleaseCom(raw);
    throw new InvalidOperationException($"GetParamStruct({kind}) вернул не {typeof(T).Name}.");
}

Добавить в начало PartModeler.cs директиву using Kompas6Constants; (рядом с существующими using).

  • Step 4: Собрать

Run: dotnet build -c Release Expected: Build succeeded, 0 ошибок.

  • Step 5: Регрессия — существующие интеграционные тесты модели

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~ModelingTests" Expected: PASS (все тесты ModelingTests зелёные — рефакторинг ничего не сломал).

  • Step 6: Commit
git add src/Kompas.Mcp.Core/Modeling/PartModeler.cs src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs
git commit -m "refactor(core): PartModeler в partial class (ядро/эскизы/операции)"

Task 2: SketchGeometry (чистые маппинги/валидация) + SketchPoint

Files:

  • Create: src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs

  • Create: src/Kompas.Mcp.Host/Tools/SketchPoint.cs

  • Test: tests/Kompas.Mcp.Tests/SketchGeometryTests.cs

  • Step 1: Написать падающий unit-тест

using Kompas.Mcp.Core.Modeling;

namespace Kompas.Mcp.Tests;

[Trait("Category", "Unit")]
public sealed class SketchGeometryTests
{
    [Theory]
    [InlineData(true, 1)]
    [InlineData(false, -1)]
    public void ArcDirection_maps_orientation(bool ccw, int expected)
        => Assert.Equal(expected, (int)SketchGeometry.ArcDirection(ccw));

    [Theory]
    [InlineData(true, false)]   // вписанный → describe=false
    [InlineData(false, true)]   // описанный → describe=true
    public void PolygonDescribe_inverts_inscribed(bool inscribed, bool expected)
        => Assert.Equal(expected, SketchGeometry.PolygonDescribe(inscribed));

    [Fact]
    public void RequirePositive_throws_on_zero_or_negative()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() => SketchGeometry.RequirePositive(0, "radius"));
        Assert.Throws<ArgumentOutOfRangeException>(() => SketchGeometry.RequirePositive(-1, "radius"));
        Assert.Null(Record.Exception(() => SketchGeometry.RequirePositive(0.1, "radius")));
    }

    [Fact]
    public void RequireVertexCount_throws_below_three()
    {
        Assert.Throws<ArgumentOutOfRangeException>(() => SketchGeometry.RequireVertexCount(2));
        Assert.Null(Record.Exception(() => SketchGeometry.RequireVertexCount(3)));
    }

    [Fact]
    public void RequirePoints_throws_on_null()
        => Assert.Throws<ArgumentNullException>(
            () => SketchGeometry.RequirePoints(null!, 2, "points"));

    [Fact]
    public void RequirePoints_throws_when_too_few()
        => Assert.Throws<ArgumentException>(
            () => SketchGeometry.RequirePoints(new[] { (0.0, 0.0) }, 2, "points"));

    [Fact]
    public void RequirePoints_throws_on_non_finite()
        => Assert.Throws<ArgumentException>(
            () => SketchGeometry.RequirePoints(new[] { (0.0, 0.0), (double.NaN, 1.0) }, 2, "points"));

    [Fact]
    public void RequirePoints_passes_on_valid()
        => Assert.Null(Record.Exception(
            () => SketchGeometry.RequirePoints(new[] { (0.0, 0.0), (1.0, 1.0) }, 2, "points")));
}
  • Step 2: Запустить — убедиться, что не компилируется/падает

Run: dotnet test --filter "FullyQualifiedName~SketchGeometryTests" Expected: FAIL (тип SketchGeometry не существует).

  • Step 3: Реализовать SketchGeometry.cs
namespace Kompas.Mcp.Core.Modeling;

/// <summary>Чистые помощники геометрии эскиза: маппинги контракта в параметры COM и валидация.</summary>
public static class SketchGeometry
{
    /// <summary>Направление дуги для ksArcByAngle: против часовой → 1, по часовой → -1.</summary>
    public static short ArcDirection(bool counterClockwise) => (short)(counterClockwise ? 1 : -1);

    /// <summary>describe для ksRegularPolygon: вписанный (вершины на окружности) → false; описанный → true.</summary>
    public static bool PolygonDescribe(bool inscribed) => !inscribed;

    /// <summary>Требовать строго положительное значение (радиус, полуось).</summary>
    public static void RequirePositive(double value, string paramName)
    {
        if (!(value > 0))
            throw new ArgumentOutOfRangeException(paramName, value, "Значение должно быть > 0.");
    }

    /// <summary>Требовать >= 3 вершин для правильного многоугольника.</summary>
    public static void RequireVertexCount(int count)
    {
        if (count < 3)
            throw new ArgumentOutOfRangeException(nameof(count), count, "Число вершин должно быть >= 3.");
    }

    /// <summary>Валидировать список точек: не null, минимум <paramref name="min"/>, все координаты конечны.</summary>
    public static void RequirePoints(IReadOnlyList<(double x, double y)> points, int min, string paramName)
    {
        ArgumentNullException.ThrowIfNull(points, paramName);
        if (points.Count < min)
            throw new ArgumentException($"Нужно минимум {min} точек, передано {points.Count}.", paramName);
        for (int i = 0; i < points.Count; i++)
            if (!double.IsFinite(points[i].x) || !double.IsFinite(points[i].y))
                throw new ArgumentException($"Точка [{i}] имеет неконечную координату.", paramName);
    }
}
  • Step 4: Реализовать SketchPoint.cs (тип параметра MCP для polyline/spline)
using System.ComponentModel;
using System.Text.Json.Serialization;

namespace Kompas.Mcp.Host.Tools;

/// <summary>Точка эскиза в его плоскости (мм). Элемент списка для ломаной/сплайна.
/// JSON-имена зафиксированы lowercase (x/y), чтобы схема инструмента совпадала с контрактом.</summary>
public sealed record SketchPoint(
    [property: JsonPropertyName("x")][property: Description("Координата X в плоскости эскиза, мм")] double X,
    [property: JsonPropertyName("y")][property: Description("Координата Y в плоскости эскиза, мм")] double Y);
  • Step 5: Запустить unit-тесты

Run: dotnet test --filter "FullyQualifiedName~SketchGeometryTests" Expected: PASS (все тесты SketchGeometryTests).

  • Step 6: Commit
git add src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs src/Kompas.Mcp.Host/Tools/SketchPoint.cs tests/Kompas.Mcp.Tests/SketchGeometryTests.cs
git commit -m "feat(core): SketchGeometry (маппинги/валидация) + SketchPoint"

Task 3: Дуга по трём точкам

Files:

  • Modify: src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs

  • Modify: src/Kompas.Mcp.Host/Tools/SketchTools.cs

  • Test: tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs

  • Step 1: Создать файл интеграционных тестов с падающим тестом

using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Query;

namespace Kompas.Mcp.Tests.Integration;

/// <summary>Интеграция: новые примитивы эскиза (профиль → выдавливание → объём).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class SketchPrimitivesTests
{
    private readonly DocumentService _docs;
    private readonly PartModeler _modeler;
    private readonly QueryService _query;

    public SketchPrimitivesTests(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 Arc3Points_semicircle_extrudes()
    {
        await _docs.CreateAsync(KompasDocumentType.Part);
        try
        {
            // Верхний полукруг R10: дуга (10,0)→(0,10)→(-10,0) + хорда обратно.
            var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
            await _modeler.AddArc3PointsAsync(s, 10, 0, 0, 10, -10, 0);
            await _modeler.AddLineAsync(s, -10, 0, 10, 0);
            await _modeler.CloseSketchAsync(s);
            await _modeler.ExtrudeAsync(s, depth: 5);
            await _modeler.RebuildAsync();

            var v = (await _query.GetPartInfoAsync()).Volume; // π·10²/2·5 ≈ 785.4
            var expected = Math.PI * 100 / 2 * 5;
            Assert.InRange(v, expected * 0.95, expected * 1.05);
        }
        finally { await _docs.CloseAsync(save: false); }
    }
}
  • Step 2: Запустить — убедиться, что падает

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Arc3Points_semicircle_extrudes" Expected: FAIL (метод AddArc3PointsAsync не существует).

  • Step 3: Реализовать метод моделиPartModeler.Sketch.cs)
/// <summary>Добавить дугу по трём точкам (начало, точка на дуге, конец) в открытый эскиз.</summary>
public Task AddArc3PointsAsync(int sketchId, double x1, double y1, double x2, double y2, double x3, double y3, CancellationToken ct = default)
    => _dispatcher.InvokeAsync(() =>
    {
        var editor = RequireOpenSketch(sketchId).Editor!;
        if (editor.ksArcBy3Points(x1, y1, x2, y2, x3, y3, MainLineStyle) == 0)
            throw new InvalidOperationException("ksArcBy3Points вернул 0 (дуга не создана — три точки коллинеарны?).");
    }, ct);
  • Step 4: Реализовать инструментSketchTools.cs, перед Close)
[McpServerTool(Name = "sketch_add_arc_3points")]
[Description("Добавить дугу по трём точкам (начало, промежуточная точка на дуге, конец) в открытый эскиз. Координаты — в плоскости эскиза, мм.")]
public async Task<string> AddArc3Points(int sketchId, double x1, double y1, double x2, double y2, double x3, double y3)
{
    await session.ConnectAsync();
    await modeler.AddArc3PointsAsync(sketchId, x1, y1, x2, y2, x3, y3);
    return "Дуга по 3 точкам добавлена.";
}
  • Step 5: Запустить тест

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Arc3Points_semicircle_extrudes" Expected: PASS.

  • Step 6: Commit
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
git commit -m "feat(sketch): дуга по 3 точкам (sketch_add_arc_3points)"

Task 4: Дуга по центру и углам

Files:

  • Modify: src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs

  • Modify: src/Kompas.Mcp.Host/Tools/SketchTools.cs

  • Test: tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs

  • Step 1: Добавить падающий тестSketchPrimitivesTests)

[Fact]
public async Task ArcByAngle_quarter_sector_extrudes()
{
    await _docs.CreateAsync(KompasDocumentType.Part);
    try
    {
        // Сектор 90° R10 в первом квадранте: дуга (10,0)→(0,10) + два радиуса к центру.
        var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
        await _modeler.AddArcByAngleAsync(s, 0, 0, 10, 0, 90, counterClockwise: true);
        await _modeler.AddLineAsync(s, 0, 10, 0, 0);
        await _modeler.AddLineAsync(s, 0, 0, 10, 0);
        await _modeler.CloseSketchAsync(s);
        await _modeler.ExtrudeAsync(s, depth: 5);
        await _modeler.RebuildAsync();

        var v = (await _query.GetPartInfoAsync()).Volume; // π·10²/4·5 ≈ 392.7
        var expected = Math.PI * 100 / 4 * 5;
        Assert.InRange(v, expected * 0.95, expected * 1.05);
    }
    finally { await _docs.CloseAsync(save: false); }
}
  • Step 2: Запустить — убедиться, что падает

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.ArcByAngle_quarter_sector_extrudes" Expected: FAIL (метод AddArcByAngleAsync не существует).

  • Step 3: Реализовать метод моделиPartModeler.Sketch.cs)
/// <summary>Добавить дугу по центру, радиусу и углам (градусы) в открытый эскиз.</summary>
public Task AddArcByAngleAsync(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true, CancellationToken ct = default)
    => _dispatcher.InvokeAsync(() =>
    {
        SketchGeometry.RequirePositive(radius, nameof(radius));
        var editor = RequireOpenSketch(sketchId).Editor!;
        var dir = SketchGeometry.ArcDirection(counterClockwise);
        if (editor.ksArcByAngle(centerX, centerY, radius, startAngle, endAngle, dir, MainLineStyle) == 0)
            throw new InvalidOperationException("ksArcByAngle вернул 0 (дуга не создана).");
    }, ct);
  • Step 4: Реализовать инструментSketchTools.cs)
[McpServerTool(Name = "sketch_add_arc")]
[Description("Добавить дугу по центру, радиусу и углам (в градусах, от оси X) в открытый эскиз. counterClockwise=true — против часовой стрелки, false — по часовой. Координаты центра — в плоскости эскиза, мм.")]
public async Task<string> AddArc(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true)
{
    await session.ConnectAsync();
    await modeler.AddArcByAngleAsync(sketchId, centerX, centerY, radius, startAngle, endAngle, counterClockwise);
    return "Дуга добавлена.";
}
  • Step 5: Запустить тест

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.ArcByAngle_quarter_sector_extrudes" Expected: PASS.

  • Step 6: Commit
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
git commit -m "feat(sketch): дуга по центру и углам (sketch_add_arc)"

Task 5: Эллипс

Files:

  • Modify: src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs

  • Modify: src/Kompas.Mcp.Host/Tools/SketchTools.cs

  • Test: tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs

  • Step 1: Добавить падающий тест

[Fact]
public async Task Ellipse_extrudes_to_expected_volume()
{
    await _docs.CreateAsync(KompasDocumentType.Part);
    try
    {
        var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
        await _modeler.AddEllipseAsync(s, 0, 0, semiMajor: 10, semiMinor: 5);
        await _modeler.CloseSketchAsync(s);
        await _modeler.ExtrudeAsync(s, depth: 4);
        await _modeler.RebuildAsync();

        var v = (await _query.GetPartInfoAsync()).Volume; // π·10·5·4 ≈ 628.3
        var expected = Math.PI * 10 * 5 * 4;
        Assert.InRange(v, expected * 0.97, expected * 1.03);
    }
    finally { await _docs.CloseAsync(save: false); }
}
  • Step 2: Запустить — убедиться, что падает

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Ellipse_extrudes_to_expected_volume" Expected: FAIL (метод AddEllipseAsync не существует).

  • Step 3: Реализовать метод моделиPartModeler.Sketch.cs). Использует NewParam<T> из Task 1; требует using Kompas6API5; (уже есть в файле).
/// <summary>Добавить эллипс: центр, полуоси A/B (мм), угол наклона большой оси к X (градусы).</summary>
public Task AddEllipseAsync(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0, CancellationToken ct = default)
    => _dispatcher.InvokeAsync(() =>
    {
        SketchGeometry.RequirePositive(semiMajor, nameof(semiMajor));
        SketchGeometry.RequirePositive(semiMinor, nameof(semiMinor));
        var editor = RequireOpenSketch(sketchId).Editor!;
        ksEllipseParam? p = null;
        try
        {
            p = NewParam<ksEllipseParam>(Kompas6Constants.StructType2DEnum.ko_EllipseParam);
            // ВНИМАНИЕ: interop генерирует свойства A/B с ЗАГЛАВНОЙ (не a/b как в SDK-доках). Регистр в C# важен.
            p.xc = centerX; p.yc = centerY;
            p.A = semiMajor; p.B = semiMinor;
            p.angle = angle; p.style = MainLineStyle;
            if (editor.ksEllipse(p) == 0)
                throw new InvalidOperationException("ksEllipse вернул 0 (эллипс не создан).");
        }
        finally { ReleaseCom(p); }
    }, ct);

Добавить в начало PartModeler.Sketch.cs директиву using Kompas6Constants;.

  • Step 4: Реализовать инструментSketchTools.cs)
[McpServerTool(Name = "sketch_add_ellipse")]
[Description("Добавить эллипс в открытый эскиз: центр, большая полуось semiMajor, малая полуось semiMinor (мм), угол наклона большой оси к X (градусы, по умолчанию 0).")]
public async Task<string> AddEllipse(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0)
{
    await session.ConnectAsync();
    await modeler.AddEllipseAsync(sketchId, centerX, centerY, semiMajor, semiMinor, angle);
    return "Эллипс добавлен.";
}
  • Step 5: Запустить тест

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Ellipse_extrudes_to_expected_volume" Expected: PASS.

  • Step 6: Commit
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
git commit -m "feat(sketch): эллипс (sketch_add_ellipse)"

Task 6: Ломаная (polyline)

Files:

  • Modify: src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs

  • Modify: src/Kompas.Mcp.Host/Tools/SketchTools.cs

  • Test: tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs

  • Step 1: Добавить падающий тест

[Fact]
public async Task Polyline_closed_triangle_extrudes()
{
    await _docs.CreateAsync(KompasDocumentType.Part);
    try
    {
        // Прямоугольный треугольник (0,0)-(20,0)-(0,15), площадь 150.
        var pts = new (double x, double y)[] { (0, 0), (20, 0), (0, 15) };
        var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
        await _modeler.AddPolylineAsync(s, pts, closed: true);
        await _modeler.CloseSketchAsync(s);
        await _modeler.ExtrudeAsync(s, depth: 5);
        await _modeler.RebuildAsync();

        var v = (await _query.GetPartInfoAsync()).Volume; // 150·5 = 750
        Assert.InRange(v, 750 * 0.97, 750 * 1.03);
    }
    finally { await _docs.CloseAsync(save: false); }
}
  • Step 2: Запустить — убедиться, что падает

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Polyline_closed_triangle_extrudes" Expected: FAIL (метод AddPolylineAsync не существует).

  • Step 3: Реализовать метод моделиPartModeler.Sketch.cs)
/// <summary>Добавить ломаную (цепочку отрезков) по списку точек. closed замыкает последнюю с первой.</summary>
public Task AddPolylineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
    => _dispatcher.InvokeAsync(() =>
    {
        // closed-ломаная из 2 точек не образует площадь — требуем минимум 3.
        SketchGeometry.RequirePoints(points, closed ? 3 : 2, nameof(points));
        var editor = RequireOpenSketch(sketchId).Editor!;
        for (int i = 0; i + 1 < points.Count; i++)
            Seg(editor, points[i], points[i + 1]);
        if (closed)
            Seg(editor, points[^1], points[0]);

        static void Seg(ksDocument2D ed, (double x, double y) a, (double x, double y) b)
        {
            if (ed.ksLineSeg(a.x, a.y, b.x, b.y, MainLineStyle) == 0)
                throw new InvalidOperationException("ksLineSeg вернул 0 (сегмент ломаной не создан).");
        }
    }, ct);
  • Step 4: Реализовать инструментSketchTools.cs). Добавить также приватный helper Map (используется и для сплайна в Task 8).
[McpServerTool(Name = "sketch_add_polyline")]
[Description("Добавить ломаную — цепочку прямых отрезков по списку точек — в открытый эскиз. closed=true замыкает последнюю точку с первой (тогда нужно минимум 3 точки). Координаты — в плоскости эскиза, мм.")]
public async Task<string> AddPolyline(int sketchId, SketchPoint[] points, bool closed = false)
{
    await session.ConnectAsync();
    await modeler.AddPolylineAsync(sketchId, Map(points), closed);
    return $"Ломаная из {points.Length} точек добавлена{(closed ? " (замкнута)" : "")}.";
}

private static IReadOnlyList<(double x, double y)> Map(SketchPoint[] points)
{
    ArgumentNullException.ThrowIfNull(points);
    return points.Select(p => (p.X, p.Y)).ToList();
}
  • Step 5: Запустить тест

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Polyline_closed_triangle_extrudes" Expected: PASS.

  • Step 6: Commit
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
git commit -m "feat(sketch): ломаная по списку точек (sketch_add_polyline)"

Task 7: Правильный многоугольник

Files:

  • Modify: src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs

  • Modify: src/Kompas.Mcp.Host/Tools/SketchTools.cs

  • Test: tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs

  • Step 1: Добавить падающий тест

[Fact]
public async Task Polygon_hexagon_extrudes_to_expected_volume()
{
    await _docs.CreateAsync(KompasDocumentType.Part);
    try
    {
        // Правильный 6-угольник, вписанный в окружность R10: площадь = 0.5·6·R²·sin(60°) ≈ 259.8.
        var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
        await _modeler.AddPolygonAsync(s, 0, 0, vertexCount: 6, radius: 10, inscribed: true);
        await _modeler.CloseSketchAsync(s);
        await _modeler.ExtrudeAsync(s, depth: 5);
        await _modeler.RebuildAsync();

        var v = (await _query.GetPartInfoAsync()).Volume;
        var area = 0.5 * 6 * 100 * Math.Sin(2 * Math.PI / 6);
        Assert.InRange(v, area * 5 * 0.95, area * 5 * 1.05);
    }
    finally { await _docs.CloseAsync(save: false); }
}
  • Step 2: Запустить — убедиться, что падает

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Polygon_hexagon_extrudes_to_expected_volume" Expected: FAIL (метод AddPolygonAsync не существует).

  • Step 3: Реализовать метод моделиPartModeler.Sketch.cs)
/// <summary>Добавить правильный многоугольник: центр, vertexCount вершин, radius (мм),
/// inscribed=true — вершины на окружности (вписанный), false — стороны касаются (описанный),
/// angle — поворот первой вершины (градусы).</summary>
public Task AddPolygonAsync(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0, CancellationToken ct = default)
    => _dispatcher.InvokeAsync(() =>
    {
        SketchGeometry.RequireVertexCount(vertexCount);
        SketchGeometry.RequirePositive(radius, nameof(radius));
        var editor = RequireOpenSketch(sketchId).Editor!;
        ksRegularPolygonParam? p = null;
        try
        {
            p = NewParam<ksRegularPolygonParam>(Kompas6Constants.StructType2DEnum.ko_RegularPolygonParam);
            p.count = vertexCount; p.xc = centerX; p.yc = centerY;
            p.radius = radius; p.ang = angle;
            p.describe = SketchGeometry.PolygonDescribe(inscribed);
            p.style = MainLineStyle;
            if (editor.ksRegularPolygon(p, 0) == 0)
                throw new InvalidOperationException("ksRegularPolygon вернул 0 (многоугольник не создан).");
        }
        finally { ReleaseCom(p); }
    }, ct);
  • Step 4: Реализовать инструментSketchTools.cs)
[McpServerTool(Name = "sketch_add_polygon")]
[Description("Добавить правильный многоугольник в открытый эскиз: центр, vertexCount вершин (>=3), radius (мм). inscribed=true — вершины лежат на окружности радиуса (вписанный в окружность); false — стороны касаются окружности (описанный). angle — поворот первой вершины (градусы, по умолчанию 0).")]
public async Task<string> AddPolygon(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0)
{
    await session.ConnectAsync();
    await modeler.AddPolygonAsync(sketchId, centerX, centerY, vertexCount, radius, inscribed, angle);
    return $"Правильный многоугольник ({vertexCount} вершин) добавлен.";
}
  • Step 5: Запустить тест

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Polygon_hexagon_extrudes_to_expected_volume" Expected: PASS.

  • Step 6: Commit
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
git commit -m "feat(sketch): правильный многоугольник (sketch_add_polygon)"

Task 8: Сплайн (NURBS)

Files:

  • Modify: src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs

  • Modify: src/Kompas.Mcp.Host/Tools/SketchTools.cs

  • Test: tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs

  • Step 1: Добавить падающий тест

[Fact]
public async Task Spline_closed_loop_extrudes()
{
    await _docs.CreateAsync(KompasDocumentType.Part);
    try
    {
        // Замкнутый сплайн по 4 точкам вокруг начала координат → выпуклая «капля».
        var pts = new (double x, double y)[] { (10, 0), (0, 10), (-10, 0), (0, -10) };
        var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
        await _modeler.AddSplineAsync(s, pts, closed: true);
        await _modeler.CloseSketchAsync(s);
        await _modeler.ExtrudeAsync(s, depth: 5);
        await _modeler.RebuildAsync();

        // Замкнутый кубический сплайн через 4 точки даёт гладкий вогнутый контур, вписанный
        // в габаритный квадрат 20×20 (площадь профиля ≈135 мм²). Проверяем, что тело построено
        // и объём положителен и не превышает габарит профиля (площадь ≤ 400 → V ≤ 2000).
        var v = (await _query.GetPartInfoAsync()).Volume;
        Assert.InRange(v, 50 * 5, 400 * 5);
    }
    finally { await _docs.CloseAsync(save: false); }
}
  • Step 2: Запустить — убедиться, что падает

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Spline_closed_loop_extrudes" Expected: FAIL (метод AddSplineAsync не существует).

  • Step 3: Реализовать метод моделиPartModeler.Sketch.cs). Добавить рядом приватную константу степени.
private const short SplineOrder = 4; // порядок NURBS (степень+1); 4 = кубический сплайн

/// <summary>Добавить сплайн (кубический NURBS, порядок 4) через список точек. closed замыкает кривую.</summary>
public Task AddSplineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
    => _dispatcher.InvokeAsync(() =>
    {
        SketchGeometry.RequirePoints(points, 2, nameof(points));
        var editor = RequireOpenSketch(sketchId).Editor!;
        if (editor.ksNurbs(SplineOrder, closed, MainLineStyle) == 0)
            throw new InvalidOperationException("ksNurbs вернул 0 (сплайн не открыт).");
        // ksNurbs открыл составной объект — ksEndObj ОБЯЗАТЕЛЕН в любом исходе, иначе редактор «застрянет».
        var completed = false;
        try
        {
            foreach (var (x, y) in points)
            {
                ksNurbsPointParam? np = null;
                try
                {
                    np = NewParam<ksNurbsPointParam>(Kompas6Constants.StructType2DEnum.ko_NurbsPointParam);
                    np.x = x; np.y = y; np.weight = 1.0;
                    if (editor.ksNurbsPoint(np) == 0)
                        throw new InvalidOperationException("ksNurbsPoint вернул 0 (узел сплайна не добавлен).");
                }
                finally { ReleaseCom(np); }
            }
            completed = true;
        }
        finally
        {
            // Закрываем составной объект всегда; ошибку завершения сообщаем только на успешном пути,
            // чтобы не подменить исходное исключение из цикла.
            var end = editor.ksEndObj();
            if (completed && end == 0)
                throw new InvalidOperationException("ksEndObj вернул 0 (сплайн не завершён).");
        }
    }, ct);
  • Step 4: Реализовать инструментSketchTools.cs)
[McpServerTool(Name = "sketch_add_spline")]
[Description("Добавить сплайн (кубический NURBS) через список точек в открытый эскиз. closed=true замыкает кривую. Координаты — в плоскости эскиза, мм.")]
public async Task<string> AddSpline(int sketchId, SketchPoint[] points, bool closed = false)
{
    await session.ConnectAsync();
    await modeler.AddSplineAsync(sketchId, Map(points), closed);
    return $"Сплайн по {points.Length} точкам добавлен{(closed ? " (замкнут)" : "")}.";
}
  • Step 5: Запустить тест

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Spline_closed_loop_extrudes" Expected: PASS.

  • Step 6: Commit
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
git commit -m "feat(sketch): сплайн NURBS по списку точек (sketch_add_spline)"

Task 9: Вспомогательная точка

Files:

  • Modify: src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs

  • Modify: src/Kompas.Mcp.Host/Tools/SketchTools.cs

  • Test: tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs

  • Step 1: Добавить падающий тест (smoke: AddPointAsync сам бросает при ksPoint==0, поэтому тест доказывает успешный вызов; плюс проверяем, что точка не ломает соседний контур)

[Fact]
public async Task Point_does_not_break_sketch()
{
    await _docs.CreateAsync(KompasDocumentType.Part);
    try
    {
        // Окружность R10 + точка в центре; выдавливание должно дать цилиндр без искажений.
        var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
        await _modeler.AddCircleAsync(s, 0, 0, 10);
        await _modeler.AddPointAsync(s, 0, 0);
        await _modeler.CloseSketchAsync(s);
        await _modeler.ExtrudeAsync(s, depth: 5);
        await _modeler.RebuildAsync();

        var v = (await _query.GetPartInfoAsync()).Volume; // π·100·5 ≈ 1570.8
        var expected = Math.PI * 100 * 5;
        Assert.InRange(v, expected * 0.97, expected * 1.03);
    }
    finally { await _docs.CloseAsync(save: false); }
}
  • Step 2: Запустить — убедиться, что падает

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Point_does_not_break_sketch" Expected: FAIL (метод AddPointAsync не существует).

  • Step 3: Реализовать метод моделиPartModeler.Sketch.cs). Добавить рядом приватную константу стиля точки.
private const int PointStyle = 0; // системный стиль точки (см. SDK pstyles)

/// <summary>Добавить точку в открытый эскиз (опорная точка для построений).</summary>
public Task AddPointAsync(int sketchId, double x, double y, CancellationToken ct = default)
    => _dispatcher.InvokeAsync(() =>
    {
        var editor = RequireOpenSketch(sketchId).Editor!;
        if (editor.ksPoint(x, y, PointStyle) == 0)
            throw new InvalidOperationException("ksPoint вернул 0 (точка не создана).");
    }, ct);
  • Step 4: Реализовать инструментSketchTools.cs)
[McpServerTool(Name = "sketch_add_point")]
[Description("Добавить точку в открытый эскиз (опорная точка для построений). Координаты — в плоскости эскиза, мм.")]
public async Task<string> AddPoint(int sketchId, double x, double y)
{
    await session.ConnectAsync();
    await modeler.AddPointAsync(sketchId, x, y);
    return "Точка добавлена.";
}
  • Step 5: Запустить тест

Run: dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Point_does_not_break_sketch" Expected: PASS.

  • Step 6: Commit
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
git commit -m "feat(sketch): вспомогательная точка (sketch_add_point)"

Task 10: Полный прогон и документация

Files:

  • Modify: CLAUDE.md, README.md, docs/ARCHITECTURE.md (через навык docs-delegate)

  • Step 1: Полный прогон unit-тестов

Run: dotnet test --filter "Category=Unit" Expected: PASS (включая 4 новых SketchGeometryTests).

  • Step 2: Полный прогон интеграционных тестов (нужен запущенный КОМПАС)

Run: dotnet test --filter "Category=Integration" Expected: PASS (включая 7 новых SketchPrimitivesTests + регрессия ModelingTests).

  • Step 3: Обновить документацию через навык docs-delegate

Передать Sonnet задачу: увеличить счётчик инструментов на +7 и тестов на +11 (4 unit + 7 integration) в CLAUDE.md (раздел «Current state») — сверив текущие числа по факту; добавить 7 примитивов эскиза (дуга ×2, эллипс, ломаная, многоугольник, сплайн, точка) в описание sketch-слоя в CLAUDE.md, README.md и docs/ARCHITECTURE.md; отметить пакет A («богаче эскизы») выполненным. Не трогать docs/OPEN_QUESTIONS.md сверх упоминания, что примитивы эскиза добавлены.

  • Step 4: Commit документации
git add CLAUDE.md README.md docs/ARCHITECTURE.md
git commit -m "docs: 7 новых примитивов эскиза (пакет A) в README/ARCHITECTURE/CLAUDE"
  • Step 5: Завершение ветки — использовать навык superpowers:finishing-a-development-branch для выбора merge/PR.

Сводка контракта (для сверки типов между задачами)

Методы PartModeler (все public Task ... Async(..., CancellationToken ct = default)):

  • AddArc3PointsAsync(int sketchId, double x1, y1, x2, y2, x3, y3)
  • AddArcByAngleAsync(int sketchId, double centerX, centerY, radius, startAngle, endAngle, bool counterClockwise = true)
  • AddEllipseAsync(int sketchId, double centerX, centerY, semiMajor, semiMinor, double angle = 0)
  • AddPolylineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false)
  • AddPolygonAsync(int sketchId, double centerX, centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0)
  • AddSplineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false)
  • AddPointAsync(int sketchId, double x, double y)

SketchGeometry (public static): short ArcDirection(bool), bool PolygonDescribe(bool), void RequirePositive(double, string), void RequireVertexCount(int), void RequirePoints(IReadOnlyList<(double x, double y)>, int, string).

SketchPoint (public record, Host): record SketchPoint(double X, double Y).

MCP-инструменты (имена): sketch_add_arc_3points, sketch_add_arc, sketch_add_ellipse, sketch_add_polyline, sketch_add_polygon, sketch_add_spline, sketch_add_point.