feat: drawing_fill_title_block — основная надпись чертежа (API7)

Веха 2D-ЧЕРТЁЖ, инкремент 2 (оформление). DrawingService.FillTitleBlockAsync:
LayoutSheets.ItemByNumber[1].Stamp.Text[id].Str = text → Update(). Enum StampField
→ номер графы ГОСТ (designation=2, name=1, material=3). Заполнение нескольких
граф за вызов; перезапись (Str замещает, Clear не нужен — подтверждено спайком).
Новый инструмент drawing_fill_title_block; ReadStampCellAsync для проверки.

Ревью Codex спека учтено: Update() результат проверяется; null-checks
(LayoutSheets/sheet/Stamp); RequireActiveDrawing выделен (сохраняет cast-проверку);
ColumnId switch с дефолтным throw; счётчик = непустые поля; name=графа 1 однозначно.

Валидация — чистый StampFields (ColumnId + Collect). Тесты: +7 unit (StampFields),
+4 integration (DrawingStampTests: заполнение+reread 3 граф, перезапись, пустой
ввод, не-чертёж). Итого 123 unit + 65 integration = 188.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 17:20:16 +03:00
parent db23a1a748
commit 742a97177b
6 changed files with 265 additions and 6 deletions
+53 -5
View File
@@ -30,6 +30,48 @@ public sealed class DrawingService
double x, double y, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => CreateStandardViewsCore(partFilePath, scale, x, y), ct);
/// <summary>
/// Заполнить графы основной надписи активного чертежа: обозначение (графа 2), наименование
/// изделия (графа 1), материал (графа 3). Передаются только нужные поля (null/пусто —
/// пропускается). Возвращает число заполненных граф.
/// </summary>
public Task<int> FillTitleBlockAsync(string? designation = null, string? name = null,
string? material = null, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => FillTitleBlockCore(designation, name, material), ct);
private int FillTitleBlockCore(string? designation, string? name, string? material)
{
var fields = StampFields.Collect(designation, name, material);
if (fields.Count == 0)
throw new ArgumentException("Укажите хотя бы одно поле основной надписи (designation/name/material).");
var stamp = RequireActiveStamp();
foreach (var (columnId, text) in fields)
stamp.Text[columnId].Str = text; // Str замещает содержимое (Clear не нужен)
if (!stamp.Update())
throw new InvalidOperationException("IStamp.Update вернул FALSE — основная надпись не обновлена.");
return fields.Count;
}
/// <summary>Прочитать текст графы основной надписи (для проверки/диагностики).</summary>
public Task<string> ReadStampCellAsync(int columnId, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var stamp = RequireActiveStamp();
return stamp.Text[columnId]?.Str ?? string.Empty;
}, ct);
private IStamp RequireActiveStamp()
{
var doc2d = RequireActiveDrawing();
var sheets = doc2d.LayoutSheets
?? throw new InvalidOperationException("Не удалось получить листы оформления чертежа.");
var sheet = sheets.ItemByNumber[1] as ILayoutSheet
?? throw new InvalidOperationException("На чертеже нет листа оформления #1.");
return sheet.Stamp as IStamp
?? throw new InvalidOperationException("У листа оформления нет основной надписи (Stamp).");
}
// Стандартная тройка проекций: ksPtFront=1, ksPtUp=3, ksPtLeft=5 (ksRelativeProjectionTypeEnum).
private static readonly object[] StandardProjections = { 1, 3, 5 };
private const int ExpectedCreated = 3;
@@ -79,19 +121,25 @@ public sealed class DrawingService
}
}
private IViews RequireActiveDrawingViews()
/// <summary>
/// Получить активный чертёж как <see cref="IKompasDocument2D"/>. Раздельные ошибки: нет документа /
/// тип != ksDocumentDrawing / не приводится к IKompasDocument2D.
/// </summary>
private IKompasDocument2D RequireActiveDrawing()
{
var doc = _session.Application.ActiveDocument
?? throw new InvalidOperationException("Нет активного документа. Создайте чертёж: document_create drawing.");
if (doc.DocumentType != DocumentTypeEnum.ksDocumentDrawing)
throw new InvalidOperationException(
$"Активный документ не является чертежом (тип: {doc.DocumentType}). Создайте чертёж: document_create drawing.");
if (doc is not IKompasDocument2D doc2d)
throw new InvalidOperationException("Активный документ не приводится к IKompasDocument2D.");
return doc2d.ViewsAndLayersManager?.Views
?? throw new InvalidOperationException("Не удалось получить коллекцию видов чертежа.");
return doc as IKompasDocument2D
?? throw new InvalidOperationException("Активный документ не приводится к IKompasDocument2D.");
}
private IViews RequireActiveDrawingViews()
=> RequireActiveDrawing().ViewsAndLayersManager?.Views
?? throw new InvalidOperationException("Не удалось получить коллекцию видов чертежа.");
private static HashSet<int> CollectViewNumbers(IViews views)
{
var set = new HashSet<int>();
@@ -0,0 +1,47 @@
namespace Kompas.Mcp.Core.Drawings;
/// <summary>
/// Поле основной надписи (графа штампа ГОСТ). Маппится на номер графы <c>ksStampEnum</c>.
/// Пока поддержаны три ключевые графы; остальные (масштаб, разработал, даты…) — позже.
/// </summary>
public enum StampField
{
/// <summary>Обозначение документа (графа 2, ksStDescription).</summary>
Designation,
/// <summary>Наименование изделия (графа 1, ksStPartNumber).</summary>
Name,
/// <summary>Материал (графа 3, ksStMaterial).</summary>
Material,
}
/// <summary>Маппинг поля основной надписи → номер графы и сбор непустых полей.</summary>
public static class StampFields
{
/// <summary>Номер графы (<c>ksStampEnum</c>) для поля основной надписи.</summary>
public static int ColumnId(StampField field) => field switch
{
StampField.Designation => 2, // ksStDescription
StampField.Name => 1, // ksStPartNumber (наименование изделия)
StampField.Material => 3, // ksStMaterial
_ => throw new ArgumentOutOfRangeException(nameof(field), field, "Неизвестное поле основной надписи."),
};
/// <summary>
/// Собрать непустые (после Trim) поля в список (номер графы, текст). null/пусто/пробелы —
/// пропускаются. Чистая (без COM) — unit-тестируемо.
/// </summary>
public static IReadOnlyList<(int ColumnId, string Text)> Collect(string? designation, string? name, string? material)
{
var list = new List<(int, string)>(3);
Add(list, StampField.Designation, designation);
Add(list, StampField.Name, name);
Add(list, StampField.Material, material);
return list;
}
private static void Add(List<(int, string)> list, StampField field, string? value)
{
if (!string.IsNullOrWhiteSpace(value))
list.Add((ColumnId(field), value.Trim()));
}
}
+15
View File
@@ -26,4 +26,19 @@ public sealed class DrawingTools(KompasSession session, DrawingService drawing)
var r = await drawing.CreateStandardViewsAsync(partFilePath, scale, x, y);
return $"Создано {r.Created} стандартных вида на чертеже (всего видов: {r.Total}, масштаб {scale}).";
}
[McpServerTool(Name = "drawing_fill_title_block")]
[Description("Заполнить графы основной надписи (штампа) активного чертежа. Передавайте только нужные поля: " +
"designation — обозначение документа (графа 2); name — наименование изделия (графа 1); " +
"material — материал (графа 3). Незаданные/пустые поля пропускаются (нужно хотя бы одно). " +
"Существующий текст графы перезаписывается. Возвращает число заполненных граф.")]
public async Task<string> FillTitleBlock(
[Description("Обозначение документа (графа 2)")] string? designation = null,
[Description("Наименование изделия (графа 1)")] string? name = null,
[Description("Материал (графа 3)")] string? material = null)
{
await session.ConnectAsync();
var filled = await drawing.FillTitleBlockAsync(designation, name, material);
return $"Заполнено граф основной надписи: {filled}.";
}
}