From 5272066bef24b92b3ed98f5a30f5f3df5bc3c964 Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Wed, 27 May 2026 21:13:11 +0300 Subject: [PATCH] =?UTF-8?q?feat(drawing):=20=D1=82=D0=B5=D0=BA=D1=81=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=D1=8B=D0=B5=20=D0=BE=D0=B1=D0=BE=D0=B7=D0=BD=D0=B0?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=E2=80=94=20=D1=88=D0=B5=D1=80?= =?UTF-8?q?=D0=BE=D1=85=D0=BE=D0=B2=D0=B0=D1=82=D0=BE=D1=81=D1=82=D1=8C,?= =?UTF-8?q?=20=D1=82=D0=B5=D0=BA=D1=81=D1=82,=20=D1=82=D0=B5=D1=85.=20?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=B1=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit drawing_add_rough (знак шероховатости: ISymbols2DContainer.Roughs + IRoughParams, SignType ksRoughSignEnum, значение Ra/Rz через RoughParamText.Str), drawing_add_text (свободная надпись: IDrawingContainer.DrawingTexts + IText.Str), drawing_set_technical_requirements (блок тех. требований уровня документа: IDrawingDocument.TechnicalDemand.Text). - DrawingService: AddRoughAsync/AddTextAsync/SetTechnicalRequirementsAsync + ReadTechnicalRequirementsAsync + счётчики; RequireDrawingContainer/RequireTechnicalDemand - RoughSignType (enum + Parse/ToKompas), DrawingAnnotationResult (Value read-back из COM) - RequireNonEmptyText в DrawingValidation - 13 unit + 13 интеграционных тестов (всего 267 зелёных), сборка Release чистая Спайк подтвердил: значение шероховатости через IRoughParams.RoughParamText.Str (round-trip); текст НЕ в IView.ObjectCount (счёт по DrawingTexts.Count); тех. требования IsCreated False→True при первом Text.Str+Update, многострочно через \n. Ревью Codex спека учтено (height убран — это высота блока, не шрифт; guards; read-back возврат). Спек — docs/superpowers/specs/. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-27-drawing-text-annotations-design.md | 167 ++++++++++++++++++ .../Drawings/DrawingAnnotationResult.cs | 11 ++ .../Drawings/DrawingService.cs | 142 +++++++++++++++ .../Drawings/DrawingValidation.cs | 7 + src/Kompas.Mcp.Core/Drawings/RoughSignType.cs | 41 +++++ src/Kompas.Mcp.Host/Tools/DrawingTools.cs | 50 ++++++ .../DrawingValidationTests.cs | 11 ++ .../Integration/DrawingRoughTests.cs | 107 +++++++++++ .../Integration/DrawingTechReqTests.cs | 76 ++++++++ .../Integration/DrawingTextTests.cs | 115 ++++++++++++ tests/Kompas.Mcp.Tests/RoughSignTypesTests.cs | 29 +++ 11 files changed, 756 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-27-drawing-text-annotations-design.md create mode 100644 src/Kompas.Mcp.Core/Drawings/DrawingAnnotationResult.cs create mode 100644 src/Kompas.Mcp.Core/Drawings/RoughSignType.cs create mode 100644 tests/Kompas.Mcp.Tests/Integration/DrawingRoughTests.cs create mode 100644 tests/Kompas.Mcp.Tests/Integration/DrawingTechReqTests.cs create mode 100644 tests/Kompas.Mcp.Tests/Integration/DrawingTextTests.cs create mode 100644 tests/Kompas.Mcp.Tests/RoughSignTypesTests.cs diff --git a/docs/superpowers/specs/2026-05-27-drawing-text-annotations-design.md b/docs/superpowers/specs/2026-05-27-drawing-text-annotations-design.md new file mode 100644 index 0000000..e9c7ca8 --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-drawing-text-annotations-design.md @@ -0,0 +1,167 @@ +# Дизайн: текстовые обозначения чертежа — шероховатость, текст, тех. требования (API7) + +**Дата:** 2026-05-27 +**Статус:** дизайн согласован (все три в одном инкременте), спайк проведён, ревью Codex спека учтено — к реализации + +## Правки по ревью Codex (спек) + +- **#4** параметр `height` у `drawing_add_text` **убран:** `IDrawingText.Height` — высота блока + форматирования, НЕ размер шрифта (шрифт задаётся на уровне `ITextItem`, вне объёма). Текст ставится + стилем по умолчанию. (Снимает и #10 — валидация height не нужна.) +- **#3/#11** сервис возвращает значение, **прочитанное обратно из COM** после `Update` (rough — + `RoughParamText.Str`; text — `((IText)dt).Str`) — как `NominalValue` у размеров. Тест `Value==вход` + доказывает round-trip. Пост-Add откат `Delete` есть в коде; не форсируется тестом (для валидных + параметров COM всегда `Valid` — как решено у диаметрального; путь идентичен протестированному угловому). +- **#1** `value` шероховатости нормализуется (`null`/whitespace → `""`); возвращается нормализованное + (read-back), контракт `Value` непустой не нарушается. +- **#2/#5/#7** добавлены null/QI-guards: `Roughs`/`(IRoughParams)`/`RoughParamText`; + `(IDrawingContainer)`/`DrawingTexts`/`(IText)`; `(IDrawingDocument)`/`TechnicalDemand`/`Text` — + каждый с раздельной диагностикой (как `RequireStampCell`/`RequireSymbols2DContainer`). +- **#6** тесты текста дополнены: многострочный round-trip (`\n`), несуществующий `viewNumber>0`. +- **#8** добавлен helper `ReadTechnicalRequirementsAsync` — тест читает `td.Text.Str` и проверяет + перезапись (как `DrawingStampTests` перечитывают графы). +- **#9** подсчёт строк тех. требований нормализован: `text.Replace("\r\n","\n")`, хвостовые пустые + строки отбрасываются (`"a\n"` → 1 строка). + +## Цель + +Веха 2D-ЧЕРТЁЖ, инкремент 6. После завершения базового семейства размеров — **текстовые обозначения**: +знак шероховатости, свободная текстовая надпись, технические требования. Три инструмента покрывают +самые частые «не-размерные» элементы конструкторского чертежа. + +## Спайк: все три механизма подтверждены вживую (НЕ разучивать) + +Спайк (`_SpikeAnnotations`, прогнан на реальном КОМПАС v24, затем удалён) на коробке 40×30×20 с +тремя стандартными видами. + +### Шероховатость — на виде (`ISymbols2DContainer.Roughs`) + +``` +IRough rough = symbols.Roughs.Add(); // без параметров +rough.BranchX0 = 20; rough.BranchY0 = 25; // положение знака (ЛОКАЛЬНАЯ СК вида, мм) +rough.Angle = 0; // угол наклона оси знака (градусы) +IRoughParams rp = (IRoughParams)rough; // QI (как IDimensionText у размеров) +rp.SignType = ksRoughSignEnum.ksDeleteMaterial; // тип знака +rp.RoughParamText.Str = "Ra 1.6"; // значение (Ra/Rz) — текст +rough.Update(); // True, rough.Valid == True +``` + +**Проверено:** `Update=True`, `Valid=True`, `RoughParamText.Str` round-trip = `"Ra 1.6"`, `Roughs.Count` +→ 1. `ksRoughSignEnum`: `ksNoProcessingType=0` (без указания обработки), `ksDeleteMaterial=1` (с +удалением слоя материала), `ksWithoutDeleteMaterial=2` (без удаления). Положение — свободные координаты +(`BranchX0/Y0`); `BaseObject` (привязка к контуру) не задаём — будущее. + +### Свободный текст — на виде (`IDrawingContainer.DrawingTexts`, НЕ Symbols!) + +``` +IDrawingContainer dc = (IDrawingContainer)view; // ВНИМАНИЕ: текст в контейнере геометрии, +IDrawingText dt = dc.DrawingTexts.Add(); // а НЕ в ISymbols2DContainer +dt.X = 30; dt.Y = 45; dt.Angle = 0; // точка привязки (ЛОКАЛЬНАЯ СК вида, мм) +((IText)dt).Str = "Образец надписи"; // содержимое (QI к IText), \n — многострочно +dt.Update(); // True, dt.Valid == True +``` + +**Проверено:** `Update=True`, `Valid=True`, `Str` round-trip, `DrawingTexts.Count` → **2** (вид уже +содержал 1 текст — авто-подпись вида; проверять по ДЕЛЬТЕ before+1, не по абсолюту). `ObjectCount` +4→4 — текст **НЕ** входит в `IView.ObjectCount` (как и размеры) → проверять `DrawingTexts.Count`. + +### Технические требования — на уровне ДОКУМЕНТА (`IDrawingDocument.TechnicalDemand`) + +``` +IDrawingDocument dd = (IDrawingDocument)doc; // QI от активного IKompasDocument2D +ITechnicalDemand td = dd.TechnicalDemand; // единый блок на документ +td.Text.Str = "1. Общие допуски по ГОСТ 30893.1.\n2. Острые кромки притупить."; // \n — строки +td.Update(); // True +``` + +**Проверено:** `IsCreated` False→True (первый `Text.Str`+`Update()` создаёт блок), `Update=True`, текст +с `\n` сохранён построчно. Объект уровня документа (не вида), единственный, над основной надписью. +`Str` замещает содержимое (как у штампа) → инструмент `set` (перезапись). + +## MCP-инструменты + +| Инструмент | Параметры | Поведение | +|---|---|---| +| `drawing_add_rough` | `x,y` (положение знака), `value=""` (Ra/Rz, напр. "Ra 1.6"), `signType="delete"` (delete\|without\|none), `angle=0` (°), `viewNumber=0` | Поставить знак шероховатости на виде. Положение `x,y` в ЛСК вида (мм). `value` — текст параметра (пусто = знак без значения). Возвращает значение и номер вида. | +| `drawing_add_text` | `x,y` (точка привязки), `text`, `angle=0` (°), `viewNumber=0` | Поставить свободную текстовую надпись на виде стилем по умолчанию. `text` — содержимое (`\n` — многострочно). Возвращает текст (read-back) и номер вида. | +| `drawing_set_technical_requirements` | `text` | Задать технические требования активного чертежа (единый блок над штампом; перезаписывает прежние). `text` — строки через `\n`. Возвращает число строк. | + +## Архитектура + +В существующем `DrawingService` (namespace `Kompas.Mcp.Core.Drawings`). Шероховатость/текст — +per-view; тех. требования — per-document. + +- `AddRoughAsync(viewNumber, x, y, value, signType, angleDeg, ct)` → `DrawingAnnotationResult`. + Введём `record DrawingAnnotationResult { string Value; int ViewNumber }` (универсальный для rough/text — + `Value`=строка, прочитанная обратно из COM; `ViewNumber`). Тех. требования возвращают число строк (int). +- `AddTextAsync(viewNumber, x, y, text, angleDeg, ct)` → `DrawingAnnotationResult`. +- `SetTechnicalRequirementsAsync(text, ct)` → `int` (число строк). +- `ReadTechnicalRequirementsAsync(ct)` → `string` (для теста — `td.Text.Str`). +- Helpers: `RequireSymbols2DContainer` (есть, для rough), новый `RequireDrawingContainer(viewNumber)` + = guarded `(IDrawingContainer)FindView(...)` (для text), новый `RequireDrawingDocument()` = + guarded `(IDrawingDocument)RequireActiveDrawing()` (для тех. требований). Все QI с раздельной диагностикой. +- Счётчики для тестов: `GetViewRoughCountAsync`, `GetViewTextCountAsync`. +- Новый enum-файл `RoughSignType.cs`: `enum RoughSignType {NoProcessing, DeleteMaterial, WithoutDeleteMaterial}` + + `RoughSignTypes.Parse(string)`/`ToKompas(...)` (→ `ksRoughSignEnum`), по образцу `AngleDimensionTypes`. +- Новый файл `DrawingAnnotationResult.cs`. +- Инструменты в `DrawingTools.cs`. + +## Валидация (чистые static, unit-тест) + +- `RequireFiniteCoords` (есть) — координаты rough/text. +- Новый `RequireNonEmptyText(string, paramName)` — для `text` (drawing_add_text, тех. требования). + `value` шероховатости НЕ обязателен (знак без значения допустим; `null`/whitespace → `""`). +- `RoughSignTypes.Parse` — разбор строки (unit-тест; `ToKompas` → `ksRoughSignEnum` НЕ покрываем unit — + граница проекта, как у `AngleDimensionTypes`/`DimensionOrientations`). + +## Реализация + +**`AddRoughCore`**: нормализация `value` (`null`/whitespace → `""`) → `RequireFiniteCoords(x,y,angleDeg)` +→ `RequireSymbols2DContainer` → `symbols.Roughs` (null-check) → `.Add()` (null-check) → `BranchX0/Y0/Angle` +→ `(IRoughParams)rough` (null-check QI) → `SignType=ToKompas`, `rp.RoughParamText` (null-check) `.Str = value` +→ `Update()` (FALSE → откат `Delete`) → `Valid` (false → откат) → читаем `rp.RoughParamText.Str` обратно +→ `DrawingAnnotationResult{Value=read-back, ViewNumber}`. + +**`AddTextCore`**: `RequireNonEmptyText(text)` + `RequireFiniteCoords(x,y,angleDeg)` → +`RequireDrawingContainer` → `dc.DrawingTexts` (null-check) → `.Add()` (null-check) → `X/Y/Angle` → +`(IText)dt` (null-check QI) `.Str = text` → `Update()` (FALSE → откат `Delete`) → `Valid` (false → откат) +→ читаем `((IText)dt).Str` обратно → `DrawingAnnotationResult{Value=read-back, ViewNumber}`. + +**`SetTechnicalRequirementsCore`**: `RequireNonEmptyText(text)` → нормализация `text.Replace("\r\n","\n")` +→ `RequireDrawingDocument()` → `dd.TechnicalDemand` (null-check) → `td.Text` (null-check) `.Str = text` → +`td.Update()` (FALSE → ошибка; объект уровня документа — отката `Delete` НЕ делаем, перезапись идемпотентна) +→ вернуть число непустых-после-trim хвоста строк (`TrimEnd('\n').Split('\n').Length`). + +**`RequireDrawingContainer(viewNumber)`** = `FindView` → `(IDrawingContainer)view` (null → «вид не приводится +к IDrawingContainer»). **`RequireDrawingDocument()`** = `RequireActiveDrawing()` → `(IDrawingDocument)doc` +(null → «чертёж не приводится к IDrawingDocument»). + +RCW точечно не освобождаем (консистентно с остальным `DrawingService`; долг v2-2). + +## Тестирование + +### Unit (`RoughSignTypesTests`, `DrawingValidationTests`) +- `RoughSignTypes.Parse`: `delete/without/none` (+ рус. синонимы) → enum; неизвестное → `ArgumentException`; null → `ArgumentNullException`. +- `RequireNonEmptyText`: бросает на null/пусто/пробелы; пропускает непустое. + +### Integration (`DrawingRoughTests`, `DrawingTextTests`, `DrawingTechReqTests`; наследуют `IntegrationTestBase`) +Шероховатость: +1. **Ставится + round-trip**: `value="Ra 1.6"`, `signType=delete` → `Value=="Ra 1.6"` (read-back из COM), + попадание в целевой вид (`Roughs.Count` +1, адресация по `viewNumber`). +2. **Знак без значения**: `value=""` → ставится (Valid), `Roughs.Count` +1. +3. **Нет видов** → понятная ошибка. 4. **Несуществующий `viewNumber>0`** → ошибка. + +Текст: +5. **Ставится + round-trip**: `text` → `Value==text` (read-back), `DrawingTexts.Count` +1 (по ДЕЛЬТЕ — + вид уже мог содержать подпись), адресация по целевому `viewNumber`. +6. **Многострочный** (`"строка1\nстрока2"`) → `Value` сохраняет обе строки (round-trip `\n`). +7. **Пустой текст** → `ArgumentException` (до Add). 8. **Нет видов** → ошибка. 9. **Несуществующий `viewNumber>0`** → ошибка. + +Тех. требования: +10. **Задаются + round-trip**: многострочный текст → `lines==2`; `ReadTechnicalRequirementsAsync` возвращает + тот же текст; повторный вызов с другим текстом перезаписывает (read-back = новый, без накопления). +11. **Пустой текст** → `ArgumentException`. 12. **Активный документ не чертёж** → понятная ошибка. + +## Дальнейшее (вне спека) +- Привязка шероховатости/выносок к геометрии (`BaseObject`); выноски (`Leaders`), обозначения баз + (`Bases`), допуски формы (`Tolerances`); рамка/формат листа; ассоциативная привязка размеров. diff --git a/src/Kompas.Mcp.Core/Drawings/DrawingAnnotationResult.cs b/src/Kompas.Mcp.Core/Drawings/DrawingAnnotationResult.cs new file mode 100644 index 0000000..ebfbfad --- /dev/null +++ b/src/Kompas.Mcp.Core/Drawings/DrawingAnnotationResult.cs @@ -0,0 +1,11 @@ +namespace Kompas.Mcp.Core.Drawings; + +/// Результат простановки текстового обозначения (шероховатость/надпись) на виде. +public sealed record DrawingAnnotationResult +{ + /// Содержимое обозначения, прочитанное обратно из COM (значение Ra/Rz либо текст надписи). + public required string Value { get; init; } + + /// Номер вида, на котором поставлено обозначение. + public required int ViewNumber { get; init; } +} diff --git a/src/Kompas.Mcp.Core/Drawings/DrawingService.cs b/src/Kompas.Mcp.Core/Drawings/DrawingService.cs index ba87a2f..00d0224 100644 --- a/src/Kompas.Mcp.Core/Drawings/DrawingService.cs +++ b/src/Kompas.Mcp.Core/Drawings/DrawingService.cs @@ -287,6 +287,148 @@ public sealed class DrawingService public Task GetViewAngularDimensionCountAsync(int viewNumber, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.AngleDimensions?.Count ?? 0, ct); + // ───────────────────────── Текстовые обозначения (инкремент 6) ───────────────────────── + + /// + /// Поставить знак шероховатости на виде в точке (,) + /// (ЛОКАЛЬНАЯ СК вида, мм). — текст параметра (Ra/Rz; пусто = знак без + /// значения). — тип знака. — угол наклона + /// оси знака (°). Возвращает значение (read-back) и номер вида. + /// + public Task AddRoughAsync(int viewNumber, double x, double y, string? value, + RoughSignType signType, double angleDeg, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => AddRoughCore(viewNumber, x, y, value, signType, angleDeg), ct); + + private DrawingAnnotationResult AddRoughCore(int viewNumber, double x, double y, string? value, + RoughSignType signType, double angleDeg) + { + DrawingValidation.RequireFiniteCoords(x, y, angleDeg); + var text = string.IsNullOrWhiteSpace(value) ? string.Empty : value; + + var (view, symbols) = RequireSymbols2DContainer(viewNumber); + var roughs = symbols.Roughs + ?? throw new InvalidOperationException("Коллекция Roughs недоступна на виде."); + var rough = roughs.Add() + ?? throw new InvalidOperationException("Roughs.Add вернул null."); + try + { + rough.BranchX0 = x; rough.BranchY0 = y; rough.Angle = angleDeg; + var rp = rough as IRoughParams + ?? throw new InvalidOperationException("Знак шероховатости не приводится к IRoughParams."); + rp.SignType = RoughSignTypes.ToKompas(signType); + var paramText = rp.RoughParamText + ?? throw new InvalidOperationException("RoughParamText недоступен (текст значения)."); + paramText.Str = text; + + if (!rough.Update()) + throw new InvalidOperationException("IRough.Update вернул FALSE — знак не построен."); + if (!rough.Valid) + throw new InvalidOperationException("Знак шероховатости построен невалидным."); + + var readBack = rp.RoughParamText?.Str ?? string.Empty; // read-back из COM + return new DrawingAnnotationResult { Value = readBack, ViewNumber = view.Number }; + } + catch + { + try { rough.Delete(); } catch { /* лучшее усилие */ } + throw; + } + } + + /// Число знаков шероховатости на виде (для проверки попадания в вид). + public Task GetViewRoughCountAsync(int viewNumber, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.Roughs?.Count ?? 0, ct); + + /// + /// Поставить свободную текстовую надпись на виде в точке (,) + /// (ЛОКАЛЬНАЯ СК вида, мм) стилем по умолчанию. — содержимое (\n — многострочно). + /// — угол наклона текста (°). Возвращает текст (read-back) и номер вида. + /// + public Task AddTextAsync(int viewNumber, double x, double y, string text, + double angleDeg, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => AddTextCore(viewNumber, x, y, text, angleDeg), ct); + + private DrawingAnnotationResult AddTextCore(int viewNumber, double x, double y, string text, double angleDeg) + { + DrawingValidation.RequireNonEmptyText(text, nameof(text)); + DrawingValidation.RequireFiniteCoords(x, y, angleDeg); + + var (view, container) = RequireDrawingContainer(viewNumber); + var texts = container.DrawingTexts + ?? throw new InvalidOperationException("Коллекция DrawingTexts недоступна на виде."); + var dt = texts.Add() + ?? throw new InvalidOperationException("DrawingTexts.Add вернул null."); + try + { + dt.X = x; dt.Y = y; dt.Angle = angleDeg; + var content = dt as IText + ?? throw new InvalidOperationException("Надпись не приводится к IText."); + content.Str = text; + + if (!dt.Update()) + throw new InvalidOperationException("IDrawingText.Update вернул FALSE — надпись не построена."); + if (!dt.Valid) + throw new InvalidOperationException("Надпись построена невалидной."); + + var readBack = ((IText)dt).Str ?? string.Empty; // read-back из COM + return new DrawingAnnotationResult { Value = readBack, ViewNumber = view.Number }; + } + catch + { + try { dt.Delete(); } catch { /* лучшее усилие */ } + throw; + } + } + + /// Число текстовых надписей на виде (для проверки попадания в вид; текст в + /// IDrawingContainer, а не в IView.ObjectCount). + public Task GetViewTextCountAsync(int viewNumber, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => RequireDrawingContainer(viewNumber).Container.DrawingTexts?.Count ?? 0, ct); + + /// + /// Задать технические требования активного чертежа (единый блок над основной надписью; перезаписывает + /// прежние). — строки через \n. Возвращает число строк. + /// + public Task SetTechnicalRequirementsAsync(string text, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => SetTechnicalRequirementsCore(text), ct); + + private int SetTechnicalRequirementsCore(string text) + { + DrawingValidation.RequireNonEmptyText(text, nameof(text)); + var normalized = text.Replace("\r\n", "\n"); + + var td = RequireTechnicalDemand(); + var content = td.Text + ?? throw new InvalidOperationException("Текст технических требований недоступен (Text == null)."); + content.Str = normalized; // Str замещает содержимое (как у штампа) + if (!td.Update()) + throw new InvalidOperationException("ITechnicalDemand.Update вернул FALSE — требования не заданы."); + + // Число содержательных строк: хвостовые пустые строки отбрасываем ("a\n" → 1). + return normalized.TrimEnd('\n').Split('\n').Length; + } + + /// Прочитать текст технических требований активного чертежа (для проверки/диагностики). + public Task ReadTechnicalRequirementsAsync(CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => RequireTechnicalDemand().Text?.Str ?? string.Empty, ct); + + private ITechnicalDemand RequireTechnicalDemand() + { + var doc = RequireActiveDrawing() as IDrawingDocument + ?? throw new InvalidOperationException("Активный чертёж не приводится к IDrawingDocument."); + return doc.TechnicalDemand + ?? throw new InvalidOperationException("У чертежа недоступны технические требования (TechnicalDemand == null)."); + } + + /// Вид + его IDrawingContainer (QI от вида). Для текстовых надписей (DrawingTexts). + private (IView View, IDrawingContainer Container) RequireDrawingContainer(int viewNumber) + { + var view = FindView(viewNumber); + var container = view as IDrawingContainer + ?? throw new InvalidOperationException("Вид не приводится к IDrawingContainer."); + return (view, container); + } + /// Вид + его ISymbols2DContainer (QI от вида). Общий для линейных/диаметральных/… размеров. private (IView View, ISymbols2DContainer Symbols) RequireSymbols2DContainer(int viewNumber) { diff --git a/src/Kompas.Mcp.Core/Drawings/DrawingValidation.cs b/src/Kompas.Mcp.Core/Drawings/DrawingValidation.cs index 2ce1ef0..2cbefa9 100644 --- a/src/Kompas.Mcp.Core/Drawings/DrawingValidation.cs +++ b/src/Kompas.Mcp.Core/Drawings/DrawingValidation.cs @@ -48,6 +48,13 @@ public static class DrawingValidation throw new ArgumentOutOfRangeException(nameof(radius), "Радиус должен быть конечным и > 0."); } + /// Проверить, что текст не пустой и не только из пробелов. + public static void RequireNonEmptyText(string? text, string paramName) + { + if (string.IsNullOrWhiteSpace(text)) + throw new ArgumentException("Текст не должен быть пустым.", paramName); + } + /// Минимальное расстояние между выносными точками линейного размера (мм). private const double MinPointDistance = 1e-7; diff --git a/src/Kompas.Mcp.Core/Drawings/RoughSignType.cs b/src/Kompas.Mcp.Core/Drawings/RoughSignType.cs new file mode 100644 index 0000000..fcbaaa8 --- /dev/null +++ b/src/Kompas.Mcp.Core/Drawings/RoughSignType.cs @@ -0,0 +1,41 @@ +using Kompas6Constants; + +namespace Kompas.Mcp.Core.Drawings; + +/// Тип знака шероховатости (обёртка над ). +public enum RoughSignType +{ + /// Без указания вида обработки. + NoProcessing, + /// С удалением слоя материала. + DeleteMaterial, + /// Без удаления слоя материала. + WithoutDeleteMaterial, +} + +/// Маппинг типа знака шероховатости ↔ перечисление КОМПАС и разбор строки. +public static class RoughSignTypes +{ + public static ksRoughSignEnum ToKompas(RoughSignType type) => type switch + { + RoughSignType.NoProcessing => ksRoughSignEnum.ksNoProcessingType, + RoughSignType.DeleteMaterial => ksRoughSignEnum.ksDeleteMaterial, + RoughSignType.WithoutDeleteMaterial => ksRoughSignEnum.ksWithoutDeleteMaterial, + _ => throw new ArgumentOutOfRangeException(nameof(type), type, "Неизвестный тип знака шероховатости."), + }; + + /// Разобрать строку типа знака (англ./рус.) в . + public static RoughSignType Parse(string value) + { + ArgumentNullException.ThrowIfNull(value); + return value.Trim().ToLowerInvariant() switch + { + "delete" or "deletematerial" or "с удалением" or "с удалением материала" => RoughSignType.DeleteMaterial, + "without" or "withoutdelete" or "без удаления" or "без удаления материала" => RoughSignType.WithoutDeleteMaterial, + "none" or "noprocessing" or "без указания" or "любая" => RoughSignType.NoProcessing, + _ => throw new ArgumentException( + $"Неизвестный тип знака шероховатости '{value}'. Допустимо: delete, without, none.", + nameof(value)), + }; + } +} diff --git a/src/Kompas.Mcp.Host/Tools/DrawingTools.cs b/src/Kompas.Mcp.Host/Tools/DrawingTools.cs index b74aba3..a729a70 100644 --- a/src/Kompas.Mcp.Host/Tools/DrawingTools.cs +++ b/src/Kompas.Mcp.Host/Tools/DrawingTools.cs @@ -127,4 +127,54 @@ public sealed class DrawingTools(KompasSession session, DrawingService drawing) var r = await drawing.AddAngularDimensionAsync(viewNumber, xc, yc, x1, y1, x2, y2, x3, y3, t); return $"Угловой размер поставлен на виде {r.ViewNumber}, {r.Value:0.###}°."; } + + [McpServerTool(Name = "drawing_add_rough")] + [Description("Поставить знак шероховатости на виде активного чертежа в точке (x,y) в ЛОКАЛЬНОЙ СК вида " + + "(мм). value — текст параметра (например \"Ra 1.6\" или \"Rz 40\"; пусто = знак без значения). " + + "signType: delete (с удалением слоя материала) | without (без удаления) | none (без указания вида " + + "обработки). angle — угол наклона оси знака в градусах. viewNumber — номер вида из " + + "drawing_create_standard_views (0 = первый/главный). Возвращает значение и номер вида.")] + public async Task AddRough( + [Description("X положения знака (СК вида, мм)")] double x, + [Description("Y положения знака")] double y, + [Description("Текст параметра, напр. \"Ra 1.6\" (пусто = без значения)")] string value = "", + [Description("Тип знака: delete | without | none")] string signType = "delete", + [Description("Угол наклона оси знака, градусы")] double angle = 0, + [Description("Номер вида (0 = первый/главный)")] int viewNumber = 0) + { + await session.ConnectAsync(); + var st = RoughSignTypes.Parse(signType); + var r = await drawing.AddRoughAsync(viewNumber, x, y, value, st, angle); + var shown = string.IsNullOrEmpty(r.Value) ? "(без значения)" : r.Value; + return $"Знак шероховатости поставлен на виде {r.ViewNumber}: {shown}."; + } + + [McpServerTool(Name = "drawing_add_text")] + [Description("Поставить свободную текстовую надпись на виде активного чертежа в точке (x,y) в ЛОКАЛЬНОЙ " + + "СК вида (мм) стилем по умолчанию. text — содержимое надписи (\\n — несколько строк). angle — угол " + + "наклона текста в градусах. viewNumber — номер вида из drawing_create_standard_views (0 = первый/" + + "главный). Возвращает поставленный текст и номер вида.")] + public async Task AddText( + [Description("X точки привязки (СК вида, мм)")] double x, + [Description("Y точки привязки")] double y, + [Description("Текст надписи (\\n — многострочно)")] string text = "", + [Description("Угол наклона текста, градусы")] double angle = 0, + [Description("Номер вида (0 = первый/главный)")] int viewNumber = 0) + { + await session.ConnectAsync(); + var r = await drawing.AddTextAsync(viewNumber, x, y, text, angle); + return $"Надпись поставлена на виде {r.ViewNumber}."; + } + + [McpServerTool(Name = "drawing_set_technical_requirements")] + [Description("Задать технические требования активного чертежа — единый текстовый блок над основной " + + "надписью (ГОСТ). text — содержимое (\\n разделяет пункты/строки, например \"1. ...\\n2. ...\"). " + + "Перезаписывает прежние технические требования (не добавляет). Возвращает число строк.")] + public async Task SetTechnicalRequirements( + [Description("Текст требований (\\n — строки)")] string text) + { + await session.ConnectAsync(); + var lines = await drawing.SetTechnicalRequirementsAsync(text); + return $"Технические требования заданы ({lines} стр.)."; + } } diff --git a/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs b/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs index 8d6584d..b072e47 100644 --- a/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs +++ b/tests/Kompas.Mcp.Tests/DrawingValidationTests.cs @@ -80,4 +80,15 @@ public sealed class DrawingValidationTests [Fact] public void RequirePositiveRadius_accepts_positive() => Assert.Null(Record.Exception(() => DrawingValidation.RequirePositiveRadius(15))); + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void RequireNonEmptyText_throws_on_empty(string? text) + => Assert.Throws(() => DrawingValidation.RequireNonEmptyText(text, "text")); + + [Fact] + public void RequireNonEmptyText_accepts_non_empty() + => Assert.Null(Record.Exception(() => DrawingValidation.RequireNonEmptyText("Ra 1.6", "text"))); } diff --git a/tests/Kompas.Mcp.Tests/Integration/DrawingRoughTests.cs b/tests/Kompas.Mcp.Tests/Integration/DrawingRoughTests.cs new file mode 100644 index 0000000..d58ba0c --- /dev/null +++ b/tests/Kompas.Mcp.Tests/Integration/DrawingRoughTests.cs @@ -0,0 +1,107 @@ +using Kompas.Mcp.Core.Documents; +using Kompas.Mcp.Core.Drawings; +using Kompas.Mcp.Core.Modeling; + +namespace Kompas.Mcp.Tests.Integration; + +/// Интеграция: знак шероховатости на виде (API7 ISymbols2DContainer.Roughs + IRoughParams). +[Trait("Category", "Integration")] +[Collection(KompasCollection.Name)] +public sealed class DrawingRoughTests : IntegrationTestBase +{ + private readonly DocumentService _docs; + private readonly PartModeler _modeler; + private readonly DrawingService _drawing; + + public DrawingRoughTests(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 Rough_is_placed_with_value_and_targets_view() + { + 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.GetViewRoughCountAsync(target); + var r = await _drawing.AddRoughAsync(target, x: 20, y: 25, value: "Ra 1.6", + signType: RoughSignType.DeleteMaterial, angleDeg: 0); + var after = await _drawing.GetViewRoughCountAsync(target); + + Assert.Equal(target, r.ViewNumber); + Assert.Equal("Ra 1.6", r.Value); // read-back из COM + Assert.Equal(before + 1, after); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task Rough_without_value_is_placed() + { + var partFile = await BuildAndSaveBoxAsync(); + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150); + var before = await _drawing.GetViewRoughCountAsync(0); + var r = await _drawing.AddRoughAsync(0, x: 15, y: 15, value: "", + signType: RoughSignType.NoProcessing, angleDeg: 0); + var after = await _drawing.GetViewRoughCountAsync(0); + Assert.Equal(before + 1, after); + Assert.Equal(string.Empty, r.Value); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task No_views_throws() + { + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await Assert.ThrowsAsync(() => + _drawing.AddRoughAsync(0, x: 0, y: 0, value: "Ra 1.6", + signType: RoughSignType.DeleteMaterial, angleDeg: 0)); + } + 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.AddRoughAsync(999, x: 0, y: 0, value: "Ra 1.6", + signType: RoughSignType.DeleteMaterial, angleDeg: 0)); + } + finally { await _docs.CloseAsync(save: false); } + } +} diff --git a/tests/Kompas.Mcp.Tests/Integration/DrawingTechReqTests.cs b/tests/Kompas.Mcp.Tests/Integration/DrawingTechReqTests.cs new file mode 100644 index 0000000..d6c447f --- /dev/null +++ b/tests/Kompas.Mcp.Tests/Integration/DrawingTechReqTests.cs @@ -0,0 +1,76 @@ +using Kompas.Mcp.Core.Documents; +using Kompas.Mcp.Core.Drawings; + +namespace Kompas.Mcp.Tests.Integration; + +/// Интеграция: технические требования чертежа (API7 IDrawingDocument.TechnicalDemand). +[Trait("Category", "Integration")] +[Collection(KompasCollection.Name)] +public sealed class DrawingTechReqTests : IntegrationTestBase +{ + private readonly DocumentService _docs; + private readonly DrawingService _drawing; + + public DrawingTechReqTests(KompasFixture fx) : base(fx) + { + _docs = new DocumentService(fx.Session, fx.Dispatcher); + _drawing = new DrawingService(fx.Session, fx.Dispatcher); + } + + [Fact] + public async Task Requirements_are_set_and_read_back() + { + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + var text = "1. Общие допуски по ГОСТ 30893.1.\n2. Острые кромки притупить."; + var lines = await _drawing.SetTechnicalRequirementsAsync(text); + Assert.Equal(2, lines); + + var readBack = await _drawing.ReadTechnicalRequirementsAsync(); + Assert.Contains("Общие допуски", readBack); + Assert.Contains("Острые кромки", readBack); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task Overwrite_replaces_previous() + { + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await _drawing.SetTechnicalRequirementsAsync("1. Первая редакция."); + await _drawing.SetTechnicalRequirementsAsync("1. Вторая редакция.\n2. Ещё пункт."); + var readBack = await _drawing.ReadTechnicalRequirementsAsync(); + + Assert.Contains("Вторая редакция", readBack); + Assert.DoesNotContain("Первая редакция", readBack); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task Empty_text_throws() + { + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await Assert.ThrowsAsync(() => + _drawing.SetTechnicalRequirementsAsync(" ")); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task Active_document_not_drawing_throws() + { + await _docs.CreateAsync(KompasDocumentType.Part); // деталь, не чертёж + try + { + await Assert.ThrowsAsync(() => + _drawing.SetTechnicalRequirementsAsync("1. Требование.")); + } + finally { await _docs.CloseAsync(save: false); } + } +} diff --git a/tests/Kompas.Mcp.Tests/Integration/DrawingTextTests.cs b/tests/Kompas.Mcp.Tests/Integration/DrawingTextTests.cs new file mode 100644 index 0000000..195cf8d --- /dev/null +++ b/tests/Kompas.Mcp.Tests/Integration/DrawingTextTests.cs @@ -0,0 +1,115 @@ +using Kompas.Mcp.Core.Documents; +using Kompas.Mcp.Core.Drawings; +using Kompas.Mcp.Core.Modeling; + +namespace Kompas.Mcp.Tests.Integration; + +/// Интеграция: свободная текстовая надпись на виде (API7 IDrawingContainer.DrawingTexts + IText). +[Trait("Category", "Integration")] +[Collection(KompasCollection.Name)] +public sealed class DrawingTextTests : IntegrationTestBase +{ + private readonly DocumentService _docs; + private readonly PartModeler _modeler; + private readonly DrawingService _drawing; + + public DrawingTextTests(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 Text_is_placed_and_targets_view() + { + 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.GetViewTextCountAsync(target); + var r = await _drawing.AddTextAsync(target, x: 30, y: 45, text: "Образец надписи", angleDeg: 0); + var after = await _drawing.GetViewTextCountAsync(target); + + Assert.Equal(target, r.ViewNumber); + Assert.Equal("Образец надписи", r.Value); // read-back из COM + Assert.Equal(before + 1, after); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task Multiline_text_round_trips() + { + var partFile = await BuildAndSaveBoxAsync(); + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150); + var r = await _drawing.AddTextAsync(0, x: 10, y: 10, text: "Строка один\nСтрока два", angleDeg: 0); + Assert.Contains("Строка один", r.Value); + Assert.Contains("Строка два", r.Value); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task Empty_text_throws() + { + var partFile = await BuildAndSaveBoxAsync(); + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150); + await Assert.ThrowsAsync(() => + _drawing.AddTextAsync(0, x: 10, y: 10, text: " ", angleDeg: 0)); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task No_views_throws() + { + await _docs.CreateAsync(KompasDocumentType.Drawing); + try + { + await Assert.ThrowsAsync(() => + _drawing.AddTextAsync(0, x: 0, y: 0, text: "Текст", angleDeg: 0)); + } + 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.AddTextAsync(999, x: 0, y: 0, text: "Текст", angleDeg: 0)); + } + finally { await _docs.CloseAsync(save: false); } + } +} diff --git a/tests/Kompas.Mcp.Tests/RoughSignTypesTests.cs b/tests/Kompas.Mcp.Tests/RoughSignTypesTests.cs new file mode 100644 index 0000000..1273cae --- /dev/null +++ b/tests/Kompas.Mcp.Tests/RoughSignTypesTests.cs @@ -0,0 +1,29 @@ +using Kompas.Mcp.Core.Drawings; + +namespace Kompas.Mcp.Tests; + +[Trait("Category", "Unit")] +public sealed class RoughSignTypesTests +{ + [Theory] + [InlineData("delete", RoughSignType.DeleteMaterial)] + [InlineData("without", RoughSignType.WithoutDeleteMaterial)] + [InlineData("none", RoughSignType.NoProcessing)] + [InlineData(" DELETE ", RoughSignType.DeleteMaterial)] + [InlineData("с удалением", RoughSignType.DeleteMaterial)] + [InlineData("без удаления", RoughSignType.WithoutDeleteMaterial)] + [InlineData("без указания", RoughSignType.NoProcessing)] + public void Parse_maps_known(string value, RoughSignType expected) + => Assert.Equal(expected, RoughSignTypes.Parse(value)); + + [Fact] + public void Parse_throws_on_unknown() + => Assert.Throws(() => RoughSignTypes.Parse("polished")); + + [Fact] + public void Parse_throws_on_null() + => Assert.Throws(() => RoughSignTypes.Parse(null!)); + + // ToKompas (→ ksRoughSignEnum) намеренно НЕ покрыт unit-тестом: тестовый проект не ссылается на + // Kompas6Constants (граница проекта, как у AngleDimensionTypes/DimensionOrientations). +}