diff --git a/docs/superpowers/plans/2026-05-26-sketch-primitives.md b/docs/superpowers/plans/2026-05-26-sketch-primitives.md new file mode 100644 index 0000000..b95b1dc --- /dev/null +++ b/docs/superpowers/plans/2026-05-26-sketch-primitives.md @@ -0,0 +1,868 @@ +# 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`. + +**Регистрация:** доп. DI не нужна — `PartModeler` уже синглтон в [Program.cs](../../../src/Kompas.Mcp.Host/Program.cs:24), инструменты подхватываются `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` (тела без изменений). + +```csharp +using System.Runtime.Versioning; +using Kompas.Mcp.Core.Interop; +using Kompas6API5; +using Kompas6Constants3D; + +namespace Kompas.Mcp.Core.Modeling; + +/// PartModeler: создание эскизов и добавление 2D-примитивов. +[SupportedOSPlatform("windows")] +public sealed partial class PartModeler +{ + // (перенесённые методы эскизов) +} +``` + +- [ ] **Step 2: Создать `PartModeler.Features.cs`** с этим заголовком и перенести в него (вырезать из `PartModeler.cs`) методы: `ExtrudeAsync`, `RevolveAsync`, `FilletEdgeAsync`, `FilletEdgeIndexAsync`, `CreateFillet`, `ChamferEdgeAsync`, `ChamferEdgeIndexAsync`, `CreateChamfer`, `RebuildAsync` (тела без изменений). + +```csharp +using System.Runtime.Versioning; +using Kompas6API5; +using Kompas6Constants3D; + +namespace Kompas.Mcp.Core.Modeling; + +/// PartModeler: формообразующие операции (выдавливание, вращение, скругление, фаска). +[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): + +```csharp +/// Создать параметрическую 2D-структуру через KompasObject (API5) и привести к интерфейсу. +private T NewParam(Kompas6Constants.StructType2DEnum kind) where T : class + => _session.Kompas.GetParamStruct((short)kind) as T + ?? 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** + +```bash +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-тест** + +```csharp +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 RequireAtLeast_throws_when_too_few() + => Assert.Throws( + () => SketchGeometry.RequireAtLeast(new[] { (0.0, 0.0) }, 2, "points")); + + [Fact] + public void RequireAtLeast_passes_when_enough() + => Assert.Null(Record.Exception( + () => SketchGeometry.RequireAtLeast(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`** + +```csharp +namespace Kompas.Mcp.Core.Modeling; + +/// Чистые помощники геометрии эскиза: маппинги контракта в параметры COM и валидация. +public static class SketchGeometry +{ + /// Направление дуги для ksArcByAngle: против часовой → 1, по часовой → -1. + public static short ArcDirection(bool counterClockwise) => (short)(counterClockwise ? 1 : -1); + + /// describe для ksRegularPolygon: вписанный (вершины на окружности) → false; описанный → true. + public static bool PolygonDescribe(bool inscribed) => !inscribed; + + /// Требовать минимум точек, иначе ArgumentException. + public static void RequireAtLeast(IReadOnlyList points, int min, string paramName) + { + ArgumentNullException.ThrowIfNull(points, paramName); + if (points.Count < min) + throw new ArgumentException($"Нужно минимум {min} точек, передано {points.Count}.", paramName); + } +} +``` + +- [ ] **Step 4: Реализовать `SketchPoint.cs`** (тип параметра MCP для polyline/spline) + +```csharp +using System.ComponentModel; + +namespace Kompas.Mcp.Host.Tools; + +/// Точка эскиза в его плоскости (мм). Элемент списка для ломаной/сплайна. +public sealed record SketchPoint( + [property: Description("Координата X в плоскости эскиза, мм")] double X, + [property: Description("Координата Y в плоскости эскиза, мм")] double Y); +``` + +- [ ] **Step 5: Запустить unit-тесты** + +Run: `dotnet test --filter "FullyQualifiedName~SketchGeometryTests"` +Expected: PASS (4 теста). + +- [ ] **Step 6: Commit** + +```bash +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: Создать файл интеграционных тестов с падающим тестом** + +```csharp +using Kompas.Mcp.Core.Documents; +using Kompas.Mcp.Core.Modeling; +using Kompas.Mcp.Core.Query; + +namespace Kompas.Mcp.Tests.Integration; + +/// Интеграция: новые примитивы эскиза (профиль → выдавливание → объём). +[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`) + +```csharp +/// Добавить дугу по трём точкам (начало, точка на дуге, конец) в открытый эскиз. +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`) + +```csharp +[McpServerTool(Name = "sketch_add_arc_3points")] +[Description("Добавить дугу по трём точкам (начало, промежуточная точка на дуге, конец) в открытый эскиз. Координаты — в плоскости эскиза, мм.")] +public async Task 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** + +```bash +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`) + +```csharp +[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`) + +```csharp +/// Добавить дугу по центру, радиусу и углам (градусы) в открытый эскиз. +public Task AddArcByAngleAsync(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус должен быть > 0."); + 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`) + +```csharp +[McpServerTool(Name = "sketch_add_arc")] +[Description("Добавить дугу по центру, радиусу и углам (в градусах, от оси X) в открытый эскиз. counterClockwise=true — против часовой стрелки, false — по часовой. Координаты центра — в плоскости эскиза, мм.")] +public async Task 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** + +```bash +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: Добавить падающий тест** + +```csharp +[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` из Task 1; требует `using Kompas6API5;` (уже есть в файле). + +```csharp +/// Добавить эллипс: центр, полуоси A/B (мм), угол наклона большой оси к X (градусы). +public Task AddEllipseAsync(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + if (semiMajor <= 0) throw new ArgumentOutOfRangeException(nameof(semiMajor), "Полуось A должна быть > 0."); + if (semiMinor <= 0) throw new ArgumentOutOfRangeException(nameof(semiMinor), "Полуось B должна быть > 0."); + var editor = RequireOpenSketch(sketchId).Editor!; + ksEllipseParam? p = null; + try + { + p = NewParam(Kompas6Constants.StructType2DEnum.ko_EllipseParam); + 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`) + +```csharp +[McpServerTool(Name = "sketch_add_ellipse")] +[Description("Добавить эллипс в открытый эскиз: центр, большая полуось semiMajor, малая полуось semiMinor (мм), угол наклона большой оси к X (градусы, по умолчанию 0).")] +public async Task 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** + +```bash +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: Добавить падающий тест** + +```csharp +[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`) + +```csharp +/// Добавить ломаную (цепочку отрезков) по списку точек. closed замыкает последнюю с первой. +public Task AddPolylineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + SketchGeometry.RequireAtLeast(points, 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 && points.Count > 2) + 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). + +```csharp +[McpServerTool(Name = "sketch_add_polyline")] +[Description("Добавить ломаную (цепочку отрезков) по списку точек в открытый эскиз. closed=true замыкает последнюю точку с первой. Координаты — в плоскости эскиза, мм.")] +public async Task 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) + => 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** + +```bash +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: Добавить падающий тест** + +```csharp +[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`) + +```csharp +/// Добавить правильный многоугольник: центр, vertexCount вершин, radius (мм), +/// inscribed=true — вершины на окружности (вписанный), false — стороны касаются (описанный), +/// angle — поворот первой вершины (градусы). +public Task AddPolygonAsync(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + if (vertexCount < 3) throw new ArgumentOutOfRangeException(nameof(vertexCount), "Число вершин должно быть >= 3."); + if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус должен быть > 0."); + var editor = RequireOpenSketch(sketchId).Editor!; + ksRegularPolygonParam? p = null; + try + { + p = NewParam(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`) + +```csharp +[McpServerTool(Name = "sketch_add_polygon")] +[Description("Добавить правильный многоугольник в открытый эскиз: центр, vertexCount вершин (>=3), radius (мм). inscribed=true — вершины лежат на окружности радиуса (вписанный в окружность); false — стороны касаются окружности (описанный). angle — поворот первой вершины (градусы, по умолчанию 0).")] +public async Task 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** + +```bash +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: Добавить падающий тест** + +```csharp +[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(); + + // Площадь сплайна между ромбом (200) и кругом (314) по этим точкам — проверяем правдоподобный объём. + var v = (await _query.GetPartInfoAsync()).Volume; + Assert.InRange(v, 200 * 5 * 0.8, 314 * 5 * 1.2); + } + 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`). Добавить рядом приватную константу степени. + +```csharp +private const short SplineDegree = 3; // кубический NURBS + +/// Добавить сплайн (кубический NURBS) через список точек. closed замыкает кривую. +public Task AddSplineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + SketchGeometry.RequireAtLeast(points, 2, nameof(points)); + var editor = RequireOpenSketch(sketchId).Editor!; + if (editor.ksNurbs(SplineDegree, closed, MainLineStyle) == 0) + throw new InvalidOperationException("ksNurbs вернул 0 (сплайн не открыт)."); + foreach (var (x, y) in points) + { + ksNurbsPointParam? np = null; + try + { + np = NewParam(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); } + } + if (editor.ksEndObj() == 0) + throw new InvalidOperationException("ksEndObj вернул 0 (сплайн не завершён)."); + }, ct); +``` + +- [ ] **Step 4: Реализовать инструмент** (в `SketchTools.cs`) + +```csharp +[McpServerTool(Name = "sketch_add_spline")] +[Description("Добавить сплайн (кубический NURBS) через список точек в открытый эскиз. closed=true замыкает кривую. Координаты — в плоскости эскиза, мм.")] +public async Task 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** + +```bash +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: Добавить падающий тест** (точка контур не образует — проверяем, что она не ломает соседнюю геометрию) + +```csharp +[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`). Добавить рядом приватную константу стиля точки. + +```csharp +private const int AuxPointStyle = 0; // вспомогательная (конструктивная) точка + +/// Добавить вспомогательную (конструктивную) точку в открытый эскиз. +public Task AddPointAsync(int sketchId, double x, double y, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + var editor = RequireOpenSketch(sketchId).Editor!; + if (editor.ksPoint(x, y, AuxPointStyle) == 0) + throw new InvalidOperationException("ksPoint вернул 0 (точка не создана)."); + }, ct); +``` + +- [ ] **Step 4: Реализовать инструмент** (в `SketchTools.cs`) + +```csharp +[McpServerTool(Name = "sketch_add_point")] +[Description("Добавить вспомогательную (конструктивную) точку в открытый эскиз. Координаты — в плоскости эскиза, мм.")] +public async Task 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** + +```bash +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 документации** + +```bash +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 RequireAtLeast(IReadOnlyList, 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`.