docs(plan,spec): правки по ревью Codex — примитивы эскиза

- NURBS: порядок 4 (кубический), не degree=3; константа SplineOrder
- NewParam<T>: освобождать сырой RCW при неудачном cast
- сплайн: ksEndObj гарантированно через try/finally
- валидация вынесена в SketchGeometry (RequirePositive/RequireVertexCount/
  RequirePoints с null+finite) — unit-тестируема
- closed polyline требует >=3 точек
- SketchPoint: [JsonPropertyName] x/y; Map с null-guard
- точка: убран неверный термin «вспомогательная»; нейтральный стиль
- зафиксированы источники сигнатур (Automation=docs, типы=interop A/B)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 21:43:23 +03:00
parent ac87b81e99
commit 2eab169e28
2 changed files with 142 additions and 63 deletions
@@ -19,7 +19,11 @@ Int32 ksNurbsPoint(Object par) // par = ksNurbsPointParam { x, y, weig
Int32 ksEndObj()
Int32 ksPoint(Double x, y, Int32 style)
```
Коды структур (`Kompas6Constants.StructType2DEnum`): `ko_EllipseParam=22`, `ko_RegularPolygonParam=92`, `ko_NurbsPointParam=18`. Стиль линии «основная» = `1`; стиль вспомогательной точки = `0`.
Коды структур (`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](../../../src/Kompas.Mcp.Host/Program.cs:24), инструменты подхватываются `WithToolsFromAssembly()`.
@@ -77,10 +81,15 @@ 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
/// <summary>Создать параметрическую 2D-структуру через KompasObject (API5) и привести к интерфейсу.</summary>
/// <summary>Создать параметрическую 2D-структуру через KompasObject (API5) и привести к интерфейсу.
/// При неудачном приведении освобождает сырой RCW, чтобы не утекал COM-объект.</summary>
private T NewParam<T>(Kompas6Constants.StructType2DEnum kind) where T : class
=> _session.Kompas.GetParamStruct((short)kind) as T
?? throw new InvalidOperationException($"GetParamStruct({kind}) вернул не {typeof(T).Name}.");
{
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`).
@@ -134,14 +143,39 @@ public sealed class SketchGeometryTests
=> Assert.Equal(expected, SketchGeometry.PolygonDescribe(inscribed));
[Fact]
public void RequireAtLeast_throws_when_too_few()
=> Assert.Throws<ArgumentException>(
() => SketchGeometry.RequireAtLeast(new[] { (0.0, 0.0) }, 2, "points"));
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 RequireAtLeast_passes_when_enough()
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.RequireAtLeast(new[] { (0.0, 0.0), (1.0, 1.0) }, 2, "points")));
() => SketchGeometry.RequirePoints(new[] { (0.0, 0.0), (1.0, 1.0) }, 2, "points")));
}
```
@@ -164,12 +198,29 @@ public static class SketchGeometry
/// <summary>describe для ksRegularPolygon: вписанный (вершины на окружности) → false; описанный → true.</summary>
public static bool PolygonDescribe(bool inscribed) => !inscribed;
/// <summary>Требовать минимум <paramref name="min"/> точек, иначе ArgumentException.</summary>
public static void RequireAtLeast<T>(IReadOnlyList<T> points, int min, string paramName)
/// <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);
}
}
```
@@ -178,19 +229,21 @@ public static class SketchGeometry
```csharp
using System.ComponentModel;
using System.Text.Json.Serialization;
namespace Kompas.Mcp.Host.Tools;
/// <summary>Точка эскиза в его плоскости (мм). Элемент списка для ломаной/сплайна.</summary>
/// <summary>Точка эскиза в его плоскости (мм). Элемент списка для ломаной/сплайна.
/// JSON-имена зафиксированы lowercase (x/y), чтобы схема инструмента совпадала с контрактом.</summary>
public sealed record SketchPoint(
[property: Description("Координата X в плоскости эскиза, мм")] double X,
[property: Description("Координата Y в плоскости эскиза, мм")] double Y);
[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 (4 теста).
Expected: PASS (все тесты `SketchGeometryTests`).
- [ ] **Step 6: Commit**
@@ -346,7 +399,7 @@ Expected: FAIL (метод `AddArcByAngleAsync` не существует).
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.");
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)
@@ -423,13 +476,14 @@ Expected: FAIL (метод `AddEllipseAsync` не существует).
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.");
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;
@@ -512,11 +566,12 @@ Expected: FAIL (метод `AddPolylineAsync` не существует).
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));
// 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 && points.Count > 2)
if (closed)
Seg(editor, points[^1], points[0]);
static void Seg(ksDocument2D ed, (double x, double y) a, (double x, double y) b)
@@ -531,7 +586,7 @@ public Task AddPolylineAsync(int sketchId, IReadOnlyList<(double x, double y)> p
```csharp
[McpServerTool(Name = "sketch_add_polyline")]
[Description("Добавить ломаную (цепочку отрезков) по списку точек в открытый эскиз. closed=true замыкает последнюю точку с первой. Координаты — в плоскости эскиза, мм.")]
[Description("Добавить ломаную цепочку прямых отрезков по списку точек в открытый эскиз. closed=true замыкает последнюю точку с первой (тогда нужно минимум 3 точки). Координаты — в плоскости эскиза, мм.")]
public async Task<string> AddPolyline(int sketchId, SketchPoint[] points, bool closed = false)
{
await session.ConnectAsync();
@@ -540,7 +595,10 @@ public async Task<string> AddPolyline(int sketchId, SketchPoint[] points, bool c
}
private static IReadOnlyList<(double x, double y)> Map(SketchPoint[] points)
=> points.Select(p => (p.X, p.Y)).ToList();
{
ArgumentNullException.ThrowIfNull(points);
return points.Select(p => (p.X, p.Y)).ToList();
}
```
- [ ] **Step 5: Запустить тест**
@@ -602,8 +660,8 @@ Expected: FAIL (метод `AddPolygonAsync` не существует).
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.");
SketchGeometry.RequireVertexCount(vertexCount);
SketchGeometry.RequirePositive(radius, nameof(radius));
var editor = RequireOpenSketch(sketchId).Editor!;
ksRegularPolygonParam? p = null;
try
@@ -687,30 +745,42 @@ Expected: FAIL (метод `AddSplineAsync` не существует).
- [ ] **Step 3: Реализовать метод модели**`PartModeler.Sketch.cs`). Добавить рядом приватную константу степени.
```csharp
private const short SplineDegree = 3; // кубический NURBS
private const short SplineOrder = 4; // порядок NURBS (степень+1); 4 = кубический сплайн
/// <summary>Добавить сплайн (кубический NURBS) через список точек. closed замыкает кривую.</summary>
/// <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.RequireAtLeast(points, 2, nameof(points));
SketchGeometry.RequirePoints(points, 2, nameof(points));
var editor = RequireOpenSketch(sketchId).Editor!;
if (editor.ksNurbs(SplineDegree, closed, MainLineStyle) == 0)
if (editor.ksNurbs(SplineOrder, closed, MainLineStyle) == 0)
throw new InvalidOperationException("ksNurbs вернул 0 (сплайн не открыт).");
foreach (var (x, y) in points)
// ksNurbs открыл составной объект — ksEndObj ОБЯЗАТЕЛЕН в любом исходе, иначе редактор «застрянет».
var completed = false;
try
{
ksNurbsPointParam? np = null;
try
foreach (var (x, y) in points)
{
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 (узел сплайна не добавлен).");
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); }
}
finally { ReleaseCom(np); }
completed = true;
}
finally
{
// Закрываем составной объект всегда; ошибку завершения сообщаем только на успешном пути,
// чтобы не подменить исходное исключение из цикла.
var end = editor.ksEndObj();
if (completed && end == 0)
throw new InvalidOperationException("ksEndObj вернул 0 (сплайн не завершён).");
}
if (editor.ksEndObj() == 0)
throw new InvalidOperationException("ksEndObj вернул 0 (сплайн не завершён).");
}, ct);
```
@@ -748,7 +818,7 @@ git commit -m "feat(sketch): сплайн NURBS по списку точек (sk
- Modify: `src/Kompas.Mcp.Host/Tools/SketchTools.cs`
- Test: `tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs`
- [ ] **Step 1: Добавить падающий тест** (точка контур не образует — проверяем, что она не ломает соседнюю геометрию)
- [ ] **Step 1: Добавить падающий тест** (smoke: `AddPointAsync` сам бросает при `ksPoint==0`, поэтому тест доказывает успешный вызов; плюс проверяем, что точка не ломает соседний контур)
```csharp
[Fact]
@@ -757,7 +827,7 @@ public async Task Point_does_not_break_sketch()
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Окружность R10 + вспомогательная точка в центре; выдавливание должно дать цилиндр.
// Окружность R10 + точка в центре; выдавливание должно дать цилиндр без искажений.
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(s, 0, 0, 10);
await _modeler.AddPointAsync(s, 0, 0);
@@ -781,14 +851,14 @@ Expected: FAIL (метод `AddPointAsync` не существует).
- [ ] **Step 3: Реализовать метод модели**`PartModeler.Sketch.cs`). Добавить рядом приватную константу стиля точки.
```csharp
private const int AuxPointStyle = 0; // вспомогательная (конструктивная) точка
private const int PointStyle = 0; // системный стиль точки (см. SDK pstyles)
/// <summary>Добавить вспомогательную (конструктивную) точку в открытый эскиз.</summary>
/// <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, AuxPointStyle) == 0)
if (editor.ksPoint(x, y, PointStyle) == 0)
throw new InvalidOperationException("ksPoint вернул 0 (точка не создана).");
}, ct);
```
@@ -797,7 +867,7 @@ public Task AddPointAsync(int sketchId, double x, double y, CancellationToken ct
```csharp
[McpServerTool(Name = "sketch_add_point")]
[Description("Добавить вспомогательную (конструктивную) точку в открытый эскиз. Координаты — в плоскости эскиза, мм.")]
[Description("Добавить точку в открытый эскиз (опорная точка для построений). Координаты — в плоскости эскиза, мм.")]
public async Task<string> AddPoint(int sketchId, double x, double y)
{
await session.ConnectAsync();
@@ -861,7 +931,7 @@ git commit -m "docs: 7 новых примитивов эскиза (пакет
- `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<T>(IReadOnlyList<T>, int, string)`.
`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)`.
@@ -18,10 +18,10 @@
- Параметрические ограничения и размеры эскиза (отдельный крупный пакет).
- Скругление/фаска углов эскиза (`pCorner` у многоугольника не используем).
- Выставление `degree` сплайна наружу — фиксируем кубический (degree=3).
- Видимые точки-маркеры — точка только вспомогательная (конструктивная).
- Выставление порядка сплайна наружу — фиксируем кубический (порядок 4).
- Дополнительные кривые (`ksArcByPoint`, дуга эллипса, Bezier, trim) — в backlog «пакет A2».
## Сигнатуры COM (подтверждены по `docs/Kompas3D_SDK/`)
## Сигнатуры COM (Automation — по `docs/Kompas3D_SDK/`; типы — по рефлексии interop)
Все методы — на интерфейсе `ksDocument2D` (API5), вызываются на редакторе открытого
эскиза (`def.BeginEdit()``ksDocument2D`). Углы **в градусах**. Стиль линии `1` =
@@ -30,22 +30,28 @@
```
long ksArcBy3Points(double x1,y1, x2,y2, x3,y3, long style)
long ksArcByAngle(double xc, yc, rad, f1_град, f2_град, short direction(1=CCW|-1=CW), long style)
long ksEllipse(LPDISPATCH ksEllipseParam) // xc,yc,a,b,angle(град),style
long ksEllipse(LPDISPATCH ksEllipseParam) // interop-свойства: xc,yc,A,B,angle(град),style
long ksRegularPolygon(LPDISPATCH ksRegularPolygonParam, short centre=0)
// count,xc,yc,ang(град),radius,describe(BOOL),style
long ksNurbs(short degree, BOOL close, long style) // открыть; degree=3 кубический
long ksNurbs(short degree, BOOL close, long style) // degree = ПОРЯДОК (степень+1); 4 = кубический
long ksNurbsPoint(LPDISPATCH ksNurbsPointParam) // x,y,weight=1.0 — на каждый узел
long ksEndObj() // завершить NURBS
long ksPoint(double x, y, long style) // style=0 — вспомогательная
long ksPoint(double x, y, long style) // style=0
long ksLineSeg(double x1,y1, x2,y2, long style) // уже используется — для polyline
```
> **Источники истины.** Automation-семантика методов — из `docs/Kompas3D_SDK/`. Точные
> C#-имена/типы свойств — из рефлексии по `libs/kompas-interop/`. Где interop расходится со
> справкой, побеждает interop (компиляция регистрозависима): в частности, `ksEllipseParam`
> имеет свойства **`A`/`B`** с заглавной (в справке — `a`/`b`). NURBS `degree` — это *порядок*
> кривой (степень+1), кубический сплайн = порядок **4**.
**Param-структуры** (`ellipse`/`polygon`/`spline`) берутся через
`KompasObject.GetParamStruct(...)` (`_session.Kompas`), заполняются, передаются в метод и
**освобождаются** `ComHelper.Release` — по дисциплине COM-lifetime проекта. Это новый для
`PartModeler` паттерн (раньше — только прямые методы вроде `ksLineSeg`/`ksCircle`).
Стиль точки: `0` — вспомогательная (конструктивная опора, без маркера).
Стиль точки: `0` (системный стиль точки, см. SDK `pstyles`).
## Новые MCP-инструменты (группа Sketch)
@@ -59,7 +65,7 @@ long ksLineSeg(double x1,y1, x2,y2, long style) // уже использу
| `sketch_add_ellipse` | `sketchId, centerX, centerY, semiMajor, semiMinor, angle=0` | `ksEllipse(param)` |
| `sketch_add_polyline` | `sketchId, points:[{x,y}…], closed=false` | цепочка `ksLineSeg(...,1)` |
| `sketch_add_polygon` | `sketchId, centerX, centerY, vertexCount, radius, inscribed=true, angle=0` | `ksRegularPolygon(param, 0)` |
| `sketch_add_spline` | `sketchId, points:[{x,y}…], closed=false` | `ksNurbs(3,close,1)``ksNurbsPoint`×N → `ksEndObj()` |
| `sketch_add_spline` | `sketchId, points:[{x,y}…], closed=false` | `ksNurbs(4,close,1)``ksNurbsPoint`×N → `ksEndObj()` |
| `sketch_add_point` | `sketchId, x, y` | `ksPoint(x,y,0)` |
### Маппинги контракт → COM
@@ -68,21 +74,23 @@ long ksLineSeg(double x1,y1, x2,y2, long style) // уже использу
- `inscribed=true → describe=false` (вершины на окружности `radius`);
`inscribed=false → describe=true` (стороны касаются окружности).
- `closed → ksNurbs(close=TRUE)` / для polyline замыкающий `ksLineSeg` от последней к первой.
- Сплайн: `degree=3`, на каждый узел `ksNurbsPoint` с `weight=1.0`; узловой вектор не
задаём (КОМПАС строит автоматически).
- Сплайн: `order=4` (кубический), на каждый узел `ksNurbsPoint` с `weight=1.0`; узловой вектор
не задаём (КОМПАС строит автоматически).
### Формат списка точек
`points` — массив объектов `{x, y}` (double). Самодокументируемая JSON-схема, меньше
ошибок у LLM (порядок координат явный). Координаты — в плоскости эскиза, мм.
`points` — массив объектов `{x, y}` (double). JSON-имена зафиксированы lowercase через
`[JsonPropertyName("x"/"y")]` на record `SketchPoint(double X, double Y)`, чтобы схема
инструмента совпадала с контрактом. Координаты — в плоскости эскиза, мм.
## Валидация (до COM-вызова)
Бросаем `ArgumentException`/`ArgumentOutOfRangeException`:
Вынесена в чистые статические guard'ы `SketchGeometry` (unit-тестируемы без COM); бросают
`ArgumentException`/`ArgumentOutOfRangeException`/`ArgumentNullException`:
- `radius > 0`, `semiMajor > 0`, `semiMinor > 0`;
- `vertexCount >= 3`;
- `polyline.points.Length >= 2`;
- `spline.points.Length >= 2`.
- список точек: не `null`, все координаты конечны (`double.IsFinite`), и минимум:
`polyline` — 2 (а при `closed=true` — 3, иначе площадь не образуется); `spline` — 2.
Возврат COM-метода `0` (или `ksEndObj() == 0` для сплайна) → `InvalidOperationException`
с понятным текстом (паттерн `ksLineSeg вернул 0`).
@@ -105,9 +113,10 @@ long ksLineSeg(double x1,y1, x2,y2, long style) // уже использу
## Тестирование
**Unit** (`Category=Unit`, без COM): валидации параметров; маппинги
`counterClockwise→direction` и `inscribed→describe`. (Координат вершин многоугольника
вручную не считаем — метод нативный.)
**Unit** (`Category=Unit`, без COM): guard'ы `SketchGeometry` (`RequirePositive`,
`RequireVertexCount`, `RequirePoints` — включая null и не-finite координаты) и маппинги
`ArcDirection` (`counterClockwise→direction`), `PolygonDescribe` (`inscribed→describe`).
(Координат вершин многоугольника вручную не считаем — метод нативный.)
**Integration** (`Category=Integration`, `KompasFixture`): для каждого профилирующего
примитива — открыть эскиз → добавить примитив(ы), образующие замкнутый контур →