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`): для каждого профилирующего примитива — открыть эскиз → добавить примитив(ы), образующие замкнутый контур →