From f89ad951ae37a81a4e365f359126ce10cec17245 Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Thu, 28 May 2026 00:18:08 +0300 Subject: [PATCH] =?UTF-8?q?feat(drawing):=20=D0=BB=D0=B8=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?-=D0=B2=D1=8B=D0=BD=D0=BE=D1=81=D0=BA=D0=B0=20=D1=81=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=D0=BE=D0=BC=20(drawing=5Fadd=5Fleader)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Выноска на виде через API7 ISymbols2DContainer.Leaders. Стрелка указывает в (x,y), текст на полке у (textX,textY). shelfDirection: auto|right|left|up|down. - DrawingService.AddLeaderAsync + GetViewLeaderCountAsync - ShelfDirection (enum + Parse/ToKompas), переиспользует DrawingAnnotationResult - 13 unit + 7 интеграционных тестов (всего 328 зелёных), сборка Release чистая КЛЮЧ (спайк): новая выноска имеет 0 ответвлений — сначала (IBranchs)bl.AddBranchByPoint(0,x,y) (остриё), ИНАЧЕ КОМПАС падает RPC_E_SERVERFAULT; затем SetBranchTextPosition (полка), (ILeader)bl.TextOnShelf.Str (текст), Update. Ревью спека Codex+pi/glm-5.1+pi/kimi-k2.6 учтено (QI as?? throw, read-back ?? empty, RequireDistinctPoints, RU-алиасы, критический порядок). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../specs/2026-05-28-drawing-leader-design.md | 107 +++++++++++++ .../Drawings/DrawingService.cs | 58 +++++++ .../Drawings/ShelfDirection.cs | 51 +++++++ src/Kompas.Mcp.Host/Tools/DrawingTools.cs | 21 +++ .../Integration/DrawingLeaderTests.cs | 141 ++++++++++++++++++ .../Kompas.Mcp.Tests/ShelfDirectionsTests.cs | 32 ++++ 6 files changed, 410 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-28-drawing-leader-design.md create mode 100644 src/Kompas.Mcp.Core/Drawings/ShelfDirection.cs create mode 100644 tests/Kompas.Mcp.Tests/Integration/DrawingLeaderTests.cs create mode 100644 tests/Kompas.Mcp.Tests/ShelfDirectionsTests.cs diff --git a/docs/superpowers/specs/2026-05-28-drawing-leader-design.md b/docs/superpowers/specs/2026-05-28-drawing-leader-design.md new file mode 100644 index 0000000..526c485 --- /dev/null +++ b/docs/superpowers/specs/2026-05-28-drawing-leader-design.md @@ -0,0 +1,107 @@ +# Дизайн: линия-выноска с текстом (drawing_add_leader) через API7 + +**Дата:** 2026-05-28 +**Статус:** дизайн согласован, спайк проведён, ревью Codex + pi/glm-5.1 + pi/kimi-k2.6 учтено — к реализации + +## Правки по ревью (Codex + pi/glm-5.1 + pi/kimi-k2.6) + +- **Сигнатура** `DrawingValidation.RequireNonEmptyText(text, nameof(text))` (двухаргументный — как есть). +- **QI с диагностикой:** `bl as IBranchs ?? throw`, `bl as ILeader ?? throw` (не прямой cast); read-back + `leader.TextOnShelf?.Str ?? string.Empty`; двухшаговый null-check `Leaders` (коллекция) и `Add()` — + как `AddRoughCore`. +- **`ShelfDirections.ToKompas(Auto)` бросает `ArgumentOutOfRangeException`** (Auto обрабатывается в сервисе + ДО вызова — «не задавать ShelfDirection»; прямой вызов для Auto — ошибка). RU-алиасы `Parse`: + `авто/вправо/влево/вверх/вниз`. +- **Вырожденная выноска:** `RequireDistinctPoints(x, y, textX, textY)` (остриё ≠ полка; есть валидатор). +- **Критический порядок** (комментарий в коде): `AddBranchByPoint` ДО `SetBranchTextPosition`/`Update` + (иначе RPC_E_SERVERFAULT — выноска без ветвей). +- **Тесты:** пустой текст — `""` И `" "`; `viewNumber=0` резолвится в первый вид; путь `shelfDirection=auto` + (ShelfDirection не задаётся). XML-`` членов `ShelfDirection` — «в ЛОКАЛЬНОЙ СК вида». +- Нейминг COM-объекта — `baseLeader` (не `bl`). `GetViewLeaderCountAsync` считает ВСЕ подтипы выносок + (для v1 ок; фильтр — при добавлении ksDrPosLeader/… — в «Дальнейшее»). + +## Цель + +Веха 2D-ЧЕРТЁЖ, инкремент 9. Поставить на виде **линию-выноску** (leader) с текстовой надписью на полке — +поясняющее обозначение, указывающее стрелкой на элемент (примечание, материал детали, и т.п.). + +## Спайк: последовательность подтверждена вживую (НЕ разучивать) + +Спайк (`_SpikeLeader`, прогнан на реальном КОМПАС, затем удалён). **Ключ:** только что созданная +выноска имеет **0 ответвлений** — любые операции над ответвлением (`SetSignPosition`) и `Update` на +выноске без ветвей падают с `RPC_E_SERVERFAULT`; сначала нужно добавить ответвление через `IBranchs`. + +``` +IBaseLeader bl = symbols.Leaders.Add(DrawingObjectTypeEnum.ksDrLeader); // ksDrLeader=20 +((IBranchs)bl).AddBranchByPoint(0, x, y); // QI; ответвление 0 — остриё (куда указывает стрелка) +bl.SetBranchTextPosition(textX, textY); // положение полки/текста +((ILeader)bl).TextOnShelf.Str = text; // надпись на полке (.Str — наш interop) +// опц.: ((ILeader)bl).ShelfDirection = ksShelfDirectionEnum.ksLSRight; +bl.Update(); // True, bl.Valid == True +``` + +**Проверено:** `AddBranchByPoint(0,x,y)` → `BranchCount=1`; `Update=True`, `Valid=True`, +`TextOnShelf.Str` round-trip; `Leaders.Count` +1. Работает с явной `ShelfDirection` и без неё. +`BaseLeader` реализует `IBaseLeader`/`IDrawingObject`; `IBranchs` и `ILeader` — COM-QI от него. + +Интерфейсы: `ILeaders.Add(DrawingObjectTypeEnum)` → `BaseLeader` (для обычной выноски `ksDrLeader=20`); +`IBranchs.AddBranchByPoint(int index, double x, double y)`; `IBaseLeader.SetBranchTextPosition(x,y)` / +`Update` / `Valid` / `Delete` / `ArrowType` (`ksArrowEnum`); `ILeader.TextOnShelf` (`IText`), +`ShelfDirection` (`ksShelfDirectionEnum`: `ksLSNone=0`/`ksLSRight=1`/`ksLSUp=2`/`ksLSDown=3`/`ksLSLeft=-1`). + +## MCP-инструмент + +| Инструмент | Параметры | Поведение | +|---|---|---| +| `drawing_add_leader` | `x,y` (остриё — куда указывает стрелка), `textX,textY` (полка/якорь текста), `text`, `shelfDirection="auto"` (auto\|right\|left\|up\|down), `viewNumber=0` | Поставить выноску на виде: стрелка из полки указывает в (x,y), текст `text` на полке у (textX,textY). Координаты — ЛОКАЛЬНАЯ СК вида (мм). `shelfDirection` — направление полки (auto = по умолчанию КОМПАС). Возвращает текст (read-back) и номер вида. | + +## Архитектура + +В `DrawingService` (namespace `Kompas.Mcp.Core.Drawings`). +- `AddLeaderAsync(viewNumber, x, y, textX, textY, text, shelfDirection, ct)` → `DrawingAnnotationResult` + (переиспользуем: `Value`=read-back текст, `ViewNumber`). +- Использует существующий `RequireSymbols2DContainer(viewNumber)` (выноски в `ISymbols2DContainer.Leaders`). +- Новый файл `src/Kompas.Mcp.Core/Drawings/ShelfDirection.cs`: `enum ShelfDirection {Auto,Right,Left,Up,Down}` + (XML-`` на членах) + `ShelfDirections.Parse(string)` / `ToKompas(ShelfDirection)→ksShelfDirectionEnum` + по образцу `AngleDimensionTypes`. `Auto` → не задавать `ShelfDirection` (оставить дефолт КОМПАС). +- Счётчик `GetViewLeaderCountAsync(viewNumber)` (для теста попадания в вид). + +## Реализация + +`AddLeaderCore`: `RequireNonEmptyText(text)` + `RequireFiniteCoords(x,y,textX,textY)` → +`RequireSymbols2DContainer` → `symbols.Leaders` (null-check) → `.Add(ksDrLeader)` (null-check) → +**внутри `try/catch`+`Delete`**: `((IBranchs)bl).AddBranchByPoint(0, x, y)` (QI null-check), +`bl.SetBranchTextPosition(textX, textY)`, `((ILeader)bl).TextOnShelf` (null-check) `.Str = text`, +если `shelfDirection != Auto`: `((ILeader)bl).ShelfDirection = ShelfDirections.ToKompas(...)` → +`bl.Update()` (FALSE → откат) → `bl.Valid` (false → откат) → read-back `((ILeader)bl).TextOnShelf.Str` +→ `DrawingAnnotationResult{Value=read-back, ViewNumber}`. + +RCW точечно не освобождаем (консистентно; долг v2-2). + +## Валидация (чистые static, unit) + +- `RequireNonEmptyText(text, nameof(text))`, `RequireFiniteCoords(x,y,textX,textY)`, + `RequireDistinctPoints(x, y, textX, textY)` (остриё ≠ полка) — все уже есть. +- `ShelfDirections.Parse` — `auto/right/left/up/down` (+ рус. `авто/вправо/влево/вверх/вниз`, регистр/пробелы); + неизвестное → `ArgumentException`; null → `ArgumentNullException`. `ToKompas` (→ `ksShelfDirectionEnum`) + НЕ покрываем unit (граница проекта); бросает `ArgumentOutOfRangeException` на `Auto`. + +## Тестирование + +### Unit (`ShelfDirectionsTests`) +- `Parse`: `auto/right/left/up/down` (+ регистр, рус.) → enum; неизвестное → `ArgumentException`; null → `ArgumentNullException`. + +### Integration (`DrawingLeaderTests`; модель — коробка + стандартные виды) +1. **Ставится + round-trip + viewNumber=0**: `text="Примечание 1"`, `shelfDirection=auto`, `viewNumber=0` + → `Value=="Примечание 1"` (read-back), `viewNumber=0` резолвится в первый вид (`r.ViewNumber` совпал), + `Leaders.Count` целевого вида +1. +2. **shelfDirection=left** (не-auto путь) на целевом виде → ставится, `Leaders.Count` +1. +3. **Пустой текст** (`""` и `" "`) → `ArgumentException` (до Add). +4. **Вырожденная** (остриё совпадает с полкой) → `ArgumentException` (до Add). +5. **Нет видов** → понятная ошибка. 6. **Несуществующий `viewNumber>0`** → понятная ошибка. + +## Дальнейшее (вне спека) +- Многострочный текст полки / текст под полкой (`TextUnderShelf`); несколько ответвлений + (`AddBranchByPoint` с разными индексами); привязка к геометрии (`IBranchs.SetBaseObject`); тип стрелки + (`ArrowType`); специальные выноски (позиция/клеймо/маркер: `ksDrPosLeader/ksDrBrandLeader/ksDrMarkerLeader`). + Базы (`Bases`), допуски формы (`Tolerances`). diff --git a/src/Kompas.Mcp.Core/Drawings/DrawingService.cs b/src/Kompas.Mcp.Core/Drawings/DrawingService.cs index bbacfa3..9f382b5 100644 --- a/src/Kompas.Mcp.Core/Drawings/DrawingService.cs +++ b/src/Kompas.Mcp.Core/Drawings/DrawingService.cs @@ -482,6 +482,64 @@ public sealed class DrawingService public Task GetViewTextCountAsync(int viewNumber, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => RequireDrawingContainer(viewNumber).Container.DrawingTexts?.Count ?? 0, ct); + /// + /// Поставить линию-выноску на виде: стрелка из полки указывает в (,), + /// текст на полке у (,) — всё в + /// ЛОКАЛЬНОЙ СК вида (мм). — направление полки (Auto = по умолчанию КОМПАС). + /// Возвращает текст (read-back) и номер вида. + /// + public Task AddLeaderAsync(int viewNumber, double x, double y, double textX, + double textY, string text, ShelfDirection shelfDirection, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => AddLeaderCore(viewNumber, x, y, textX, textY, text, shelfDirection), ct); + + private DrawingAnnotationResult AddLeaderCore(int viewNumber, double x, double y, double textX, + double textY, string text, ShelfDirection shelfDirection) + { + DrawingValidation.RequireNonEmptyText(text, nameof(text)); + DrawingValidation.RequireFiniteCoords(x, y, textX, textY); + DrawingValidation.RequireDistinctPoints(x, y, textX, textY); // остриё ≠ полка (иначе вырождение) + + var (view, symbols) = RequireSymbols2DContainer(viewNumber); + var leaders = symbols.Leaders + ?? throw new InvalidOperationException("Коллекция Leaders недоступна на виде."); + var baseLeader = leaders.Add(DrawingObjectTypeEnum.ksDrLeader) + ?? throw new InvalidOperationException("Leaders.Add вернул null."); + try + { + // КРИТИЧЕСКИЙ ПОРЯДОК: AddBranchByPoint ДОЛЖЕН предшествовать SetBranchTextPosition/Update — + // выноска без ответвлений валит КОМПАС (RPC_E_SERVERFAULT). Проверено спайком. + var branches = baseLeader as IBranchs + ?? throw new InvalidOperationException("Выноска не приводится к IBranchs."); + branches.AddBranchByPoint(0, x, y); // остриё ответвления 0 (куда указывает стрелка) + baseLeader.SetBranchTextPosition(textX, textY); // положение полки/текста + + var leader = baseLeader as ILeader + ?? throw new InvalidOperationException("Выноска не приводится к ILeader."); + var shelf = leader.TextOnShelf + ?? throw new InvalidOperationException("Текст полки выноски недоступен (TextOnShelf == null)."); + shelf.Str = text; + if (shelfDirection != ShelfDirection.Auto) + leader.ShelfDirection = ShelfDirections.ToKompas(shelfDirection); + + if (!baseLeader.Update()) + throw new InvalidOperationException("ILeader.Update вернул FALSE — выноска не построена."); + if (!baseLeader.Valid) + throw new InvalidOperationException("Выноска построена невалидной."); + + var readBack = leader.TextOnShelf?.Str ?? string.Empty; // read-back из COM + return new DrawingAnnotationResult { Value = readBack, ViewNumber = view.Number }; + } + catch + { + try { baseLeader.Delete(); } catch { /* лучшее усилие */ } + throw; + } + } + + /// Число выносок на виде (для проверки попадания в вид; считает все подтипы выносок). + public Task GetViewLeaderCountAsync(int viewNumber, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.Leaders?.Count ?? 0, ct); + /// /// Задать технические требования активного чертежа (единый блок над основной надписью; перезаписывает /// прежние). — строки через \n. Возвращает число строк. diff --git a/src/Kompas.Mcp.Core/Drawings/ShelfDirection.cs b/src/Kompas.Mcp.Core/Drawings/ShelfDirection.cs new file mode 100644 index 0000000..33b0e9a --- /dev/null +++ b/src/Kompas.Mcp.Core/Drawings/ShelfDirection.cs @@ -0,0 +1,51 @@ +using Kompas6Constants; + +namespace Kompas.Mcp.Core.Drawings; + +/// Направление полки выноски (обёртка над ). +public enum ShelfDirection +{ + /// Авто — направление полки оставляет КОМПАС (свойство не задаётся). + Auto, + /// Полка вправо (в ЛОКАЛЬНОЙ СК вида). + Right, + /// Полка влево (в ЛОКАЛЬНОЙ СК вида). + Left, + /// Полка вверх (в ЛОКАЛЬНОЙ СК вида). + Up, + /// Полка вниз (в ЛОКАЛЬНОЙ СК вида). + Down, +} + +/// Маппинг направления полки ↔ перечисление КОМПАС и разбор строки. +public static class ShelfDirections +{ + /// В . Бросает на — + /// «авто» обрабатывается в сервисе (свойство не задаётся), сюда передавать нельзя. + public static ksShelfDirectionEnum ToKompas(ShelfDirection direction) => direction switch + { + ShelfDirection.Right => ksShelfDirectionEnum.ksLSRight, + ShelfDirection.Left => ksShelfDirectionEnum.ksLSLeft, + ShelfDirection.Up => ksShelfDirectionEnum.ksLSUp, + ShelfDirection.Down => ksShelfDirectionEnum.ksLSDown, + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, + "ShelfDirection.Auto не имеет COM-значения (направление не задаётся)."), + }; + + /// Разобрать строку направления (англ./рус., регистр/пробелы) в . + public static ShelfDirection Parse(string value) + { + ArgumentNullException.ThrowIfNull(value); + return value.Trim().ToLowerInvariant() switch + { + "auto" or "авто" => ShelfDirection.Auto, + "right" or "вправо" => ShelfDirection.Right, + "left" or "влево" => ShelfDirection.Left, + "up" or "вверх" => ShelfDirection.Up, + "down" or "вниз" => ShelfDirection.Down, + _ => throw new ArgumentException( + $"Неизвестное направление полки '{value}'. Допустимо: auto, right, left, up, down.", + nameof(value)), + }; + } +} diff --git a/src/Kompas.Mcp.Host/Tools/DrawingTools.cs b/src/Kompas.Mcp.Host/Tools/DrawingTools.cs index c4e5907..aefd72b 100644 --- a/src/Kompas.Mcp.Host/Tools/DrawingTools.cs +++ b/src/Kompas.Mcp.Host/Tools/DrawingTools.cs @@ -207,4 +207,25 @@ public sealed class DrawingTools(KompasSession session, DrawingService drawing) var orient = r.Landscape ? "альбомная" : "книжная"; return $"Формат листа {sheetNumber}: {r.Format}, {r.Width:0.#}×{r.Height:0.#} мм, {orient}."; } + + [McpServerTool(Name = "drawing_add_leader")] + [Description("Поставить линию-выноску с текстом на виде активного чертежа. Стрелка выноски указывает " + + "в точку (x,y); текст text размещается на полке у точки (textX,textY) — всё в ЛОКАЛЬНОЙ СК вида (мм). " + + "shelfDirection: auto (по умолчанию — КОМПАС сам) | right | left | up | down — направление полки. " + + "viewNumber — номер вида из drawing_create_standard_views (0 = первый/главный). Точка острия (x,y) " + + "должна отличаться от точки полки (textX,textY). Возвращает поставленный текст и номер вида.")] + public async Task AddLeader( + [Description("X острия — куда указывает стрелка (СК вида, мм)")] double x, + [Description("Y острия")] double y, + [Description("X полки/якоря текста")] double textX, + [Description("Y полки/якоря текста")] double textY, + [Description("Текст надписи на полке")] string text, + [Description("Направление полки: auto | right | left | up | down")] string shelfDirection = "auto", + [Description("Номер вида (0 = первый/главный)")] int viewNumber = 0) + { + await session.ConnectAsync(); + var dir = ShelfDirections.Parse(shelfDirection); + var r = await drawing.AddLeaderAsync(viewNumber, x, y, textX, textY, text, dir); + return $"Выноска поставлена на виде {r.ViewNumber}: «{r.Value}»."; + } } diff --git a/tests/Kompas.Mcp.Tests/Integration/DrawingLeaderTests.cs b/tests/Kompas.Mcp.Tests/Integration/DrawingLeaderTests.cs new file mode 100644 index 0000000..0bfe388 --- /dev/null +++ b/tests/Kompas.Mcp.Tests/Integration/DrawingLeaderTests.cs @@ -0,0 +1,141 @@ +using Kompas.Mcp.Core.Documents; +using Kompas.Mcp.Core.Drawings; +using Kompas.Mcp.Core.Modeling; + +namespace Kompas.Mcp.Tests.Integration; + +/// Интеграция: линия-выноска с текстом на виде (API7 ISymbols2DContainer.Leaders + IBranchs). +[Trait("Category", "Integration")] +[Collection(KompasCollection.Name)] +public sealed class DrawingLeaderTests : IntegrationTestBase +{ + private readonly DocumentService _docs; + private readonly PartModeler _modeler; + private readonly DrawingService _drawing; + + public DrawingLeaderTests(KompasFixture fx) : base(fx) + { + _docs = new DocumentService(fx.Session, fx.Dispatcher); + _modeler = new PartModeler(fx.Session, fx.Dispatcher); + _drawing = new DrawingService(fx.Session, fx.Dispatcher); + } + + private async Task BuildAndSaveBoxAsync() + { + var path = TestPaths.NewFile(".m3d"); + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddRectangleAsync(s, 0, 0, 40, 30); + await _modeler.CloseSketchAsync(s); + await _modeler.ExtrudeAsync(s, depth: 20); + await _modeler.RebuildAsync(); + await _docs.SaveAsAsync(path); + } + finally { await _docs.CloseAsync(save: false); } + return path; + } + + [Fact] + public async Task Leader_is_placed_round_trip_and_resolves_default_view() + { + var partFile = await BuildAndSaveBoxAsync(); + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150); + var first = views.ViewNumbers[0]; + + var before = await _drawing.GetViewLeaderCountAsync(first); + // viewNumber=0 → первый несистемный вид (= first) + var r = await _drawing.AddLeaderAsync(0, x: 5, y: 5, textX: 25, textY: 20, + text: "Примечание 1", shelfDirection: ShelfDirection.Auto); + var after = await _drawing.GetViewLeaderCountAsync(first); + + Assert.Equal(first, r.ViewNumber); + Assert.Equal("Примечание 1", r.Value); // read-back из COM + Assert.Equal(before + 1, after); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task Leader_with_explicit_shelf_direction() + { + var partFile = await BuildAndSaveBoxAsync(); + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150); + var target = views.ViewNumbers[0]; + var before = await _drawing.GetViewLeaderCountAsync(target); + var r = await _drawing.AddLeaderAsync(target, x: 10, y: 8, textX: 30, textY: 25, + text: "Слева", shelfDirection: ShelfDirection.Left); + var after = await _drawing.GetViewLeaderCountAsync(target); + Assert.Equal(before + 1, after); + Assert.Equal("Слева", r.Value); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + public async Task Empty_text_throws(string text) + { + var partFile = await BuildAndSaveBoxAsync(); + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150); + await Assert.ThrowsAsync(() => + _drawing.AddLeaderAsync(0, x: 5, y: 5, textX: 25, textY: 20, text: text, + shelfDirection: ShelfDirection.Auto)); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task Degenerate_tip_equals_shelf_throws() + { + var partFile = await BuildAndSaveBoxAsync(); + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150); + await Assert.ThrowsAsync(() => + _drawing.AddLeaderAsync(0, x: 5, y: 5, textX: 5, textY: 5, text: "X", + shelfDirection: ShelfDirection.Auto)); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task No_views_throws() + { + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await Assert.ThrowsAsync(() => + _drawing.AddLeaderAsync(0, x: 5, y: 5, textX: 25, textY: 20, text: "X", + shelfDirection: ShelfDirection.Auto)); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task Unknown_view_number_throws() + { + var partFile = await BuildAndSaveBoxAsync(); + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150); + await Assert.ThrowsAsync(() => + _drawing.AddLeaderAsync(999, x: 5, y: 5, textX: 25, textY: 20, text: "X", + shelfDirection: ShelfDirection.Auto)); + } + finally { await _docs.CloseAsync(save: false); } + } +} diff --git a/tests/Kompas.Mcp.Tests/ShelfDirectionsTests.cs b/tests/Kompas.Mcp.Tests/ShelfDirectionsTests.cs new file mode 100644 index 0000000..c4a41a6 --- /dev/null +++ b/tests/Kompas.Mcp.Tests/ShelfDirectionsTests.cs @@ -0,0 +1,32 @@ +using Kompas.Mcp.Core.Drawings; + +namespace Kompas.Mcp.Tests; + +[Trait("Category", "Unit")] +public sealed class ShelfDirectionsTests +{ + [Theory] + [InlineData("auto", ShelfDirection.Auto)] + [InlineData("right", ShelfDirection.Right)] + [InlineData("left", ShelfDirection.Left)] + [InlineData("up", ShelfDirection.Up)] + [InlineData("down", ShelfDirection.Down)] + [InlineData(" RIGHT ", ShelfDirection.Right)] + [InlineData("авто", ShelfDirection.Auto)] + [InlineData("вправо", ShelfDirection.Right)] + [InlineData("влево", ShelfDirection.Left)] + [InlineData("вверх", ShelfDirection.Up)] + [InlineData("вниз", ShelfDirection.Down)] + public void Parse_maps_known(string value, ShelfDirection expected) + => Assert.Equal(expected, ShelfDirections.Parse(value)); + + [Fact] + public void Parse_throws_on_unknown() + => Assert.Throws(() => ShelfDirections.Parse("diagonal")); + + [Fact] + public void Parse_throws_on_null() + => Assert.Throws(() => ShelfDirections.Parse(null!)); + + // ToKompas (→ ksShelfDirectionEnum) — граница проекта; для Auto бросает (проверяется косвенно). +}