diff --git a/docs/superpowers/plans/2026-05-26-sketch-primitives.md b/docs/superpowers/plans/2026-05-26-sketch-primitives.md
index b95b1dc..f552a24 100644
--- a/docs/superpowers/plans/2026-05-26-sketch-primitives.md
+++ b/docs/superpowers/plans/2026-05-26-sketch-primitives.md
@@ -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
-/// Создать параметрическую 2D-структуру через KompasObject (API5) и привести к интерфейсу.
+/// Создать параметрическую 2D-структуру через KompasObject (API5) и привести к интерфейсу.
+/// При неудачном приведении освобождает сырой RCW, чтобы не утекал COM-объект.
private T NewParam(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(
- () => SketchGeometry.RequireAtLeast(new[] { (0.0, 0.0) }, 2, "points"));
+ public void RequirePositive_throws_on_zero_or_negative()
+ {
+ Assert.Throws(() => SketchGeometry.RequirePositive(0, "radius"));
+ Assert.Throws(() => 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(() => SketchGeometry.RequireVertexCount(2));
+ Assert.Null(Record.Exception(() => SketchGeometry.RequireVertexCount(3)));
+ }
+
+ [Fact]
+ public void RequirePoints_throws_on_null()
+ => Assert.Throws(
+ () => SketchGeometry.RequirePoints(null!, 2, "points"));
+
+ [Fact]
+ public void RequirePoints_throws_when_too_few()
+ => Assert.Throws(
+ () => SketchGeometry.RequirePoints(new[] { (0.0, 0.0) }, 2, "points"));
+
+ [Fact]
+ public void RequirePoints_throws_on_non_finite()
+ => Assert.Throws(
+ () => 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
/// describe для ksRegularPolygon: вписанный (вершины на окружности) → false; описанный → true.
public static bool PolygonDescribe(bool inscribed) => !inscribed;
- /// Требовать минимум точек, иначе ArgumentException.
- public static void RequireAtLeast(IReadOnlyList points, int min, string paramName)
+ /// Требовать строго положительное значение (радиус, полуось).
+ public static void RequirePositive(double value, string paramName)
+ {
+ if (!(value > 0))
+ throw new ArgumentOutOfRangeException(paramName, value, "Значение должно быть > 0.");
+ }
+
+ /// Требовать >= 3 вершин для правильного многоугольника.
+ public static void RequireVertexCount(int count)
+ {
+ if (count < 3)
+ throw new ArgumentOutOfRangeException(nameof(count), count, "Число вершин должно быть >= 3.");
+ }
+
+ /// Валидировать список точек: не null, минимум , все координаты конечны.
+ 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;
-/// Точка эскиза в его плоскости (мм). Элемент списка для ломаной/сплайна.
+/// Точка эскиза в его плоскости (мм). Элемент списка для ломаной/сплайна.
+/// JSON-имена зафиксированы lowercase (x/y), чтобы схема инструмента совпадала с контрактом.
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(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 AddPolyline(int sketchId, SketchPoint[] points, bool closed = false)
{
await session.ConnectAsync();
@@ -540,7 +595,10 @@ public async Task 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 = кубический сплайн
-/// Добавить сплайн (кубический NURBS) через список точек. closed замыкает кривую.
+/// Добавить сплайн (кубический NURBS, порядок 4) через список точек. 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));
+ 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(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(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)
-/// Добавить вспомогательную (конструктивную) точку в открытый эскиз.
+/// Добавить точку в открытый эскиз (опорная точка для построений).
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 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(IReadOnlyList, 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)`.
diff --git a/docs/superpowers/specs/2026-05-26-sketch-primitives-design.md b/docs/superpowers/specs/2026-05-26-sketch-primitives-design.md
index 7503565..252cfe1 100644
--- a/docs/superpowers/specs/2026-05-26-sketch-primitives-design.md
+++ b/docs/superpowers/specs/2026-05-26-sketch-primitives-design.md
@@ -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`): для каждого профилирующего
примитива — открыть эскиз → добавить примитив(ы), образующие замкнутый контур →