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).
+}