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:
@@ -1,7 +1,30 @@
|
||||
# Дизайн: основная надпись чертежа (drawing_fill_title_block) через API7
|
||||
|
||||
**Дата:** 2026-05-27
|
||||
**Статус:** спайк зелёный (round-trip графы подтверждён); спроектировано; на ревью (Codex)
|
||||
**Статус:** реализовано и проверено (4 интеграционных + 7 unit, всего 188); ревью Codex спека учтено
|
||||
|
||||
## Находка спайка: `Str` ПЕРЕЗАПИСЫВАЕТ (Clear не нужен)
|
||||
|
||||
Двойная запись в графу 2 («ДЕТАЛЬ.001» → «ИЗДЕЛИЕ.002») → чтение даёт «ИЗДЕЛИЕ.002» (не
|
||||
дописано). Значит `stamp.Text[id].Str = value` **замещает** содержимое — `IText.Clear()` перед
|
||||
записью НЕ нужен.
|
||||
|
||||
## Правки по ревью Codex
|
||||
|
||||
- **Update() результат проверять**: `if (!stamp.Update()) throw`.
|
||||
- **null-checks**: `LayoutSheets`, `sheet`, `sheet.Stamp` (ранняя диагностируемая ошибка).
|
||||
- **`RequireActiveDrawing()`** сохраняет проверку `doc is not IKompasDocument2D` (не только
|
||||
`DocumentType`); `RequireActiveDrawingViews` становится надстройкой над ним.
|
||||
- **`ColumnId` switch** — дефолт `ArgumentOutOfRangeException`; unit-тест на `(StampField)999`.
|
||||
- **Тест**: писать и перечитывать ВСЕ три графы (вкл. `name`=графа 1); явный reread `Text[id].Str`;
|
||||
тест overwrite (запись дважды → читается второе) — уже подтверждён спайком, перенесём в регрессию.
|
||||
- **`name` = «Наименование изделия» (графа 1, `ksStPartNumber`)** — однозначно в описании (не путать
|
||||
с `ksStDocumentName=51`).
|
||||
- **Счётчик результата** = число непустых (trimmed) полей, записанных до успешного `Update()`.
|
||||
- **Сигнатура**: `FillTitleBlockAsync(string? designation = null, string? name = null,
|
||||
string? material = null, ct)`.
|
||||
- **Scale (графа 6) и прочие — ЯВНО вне scope инкремента 2.** Spike-метод и spike-тест удаляются.
|
||||
- RCW точечно не освобождаем (v2-2).
|
||||
|
||||
## Цель
|
||||
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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}.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Drawings;
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
|
||||
namespace Kompas.Mcp.Tests.Integration;
|
||||
|
||||
/// <summary>Интеграция: заполнение основной надписи чертежа (API7 IStamp.Text).</summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Collection(KompasCollection.Name)]
|
||||
public sealed class DrawingStampTests : IntegrationTestBase
|
||||
{
|
||||
private readonly DocumentService _docs;
|
||||
private readonly DrawingService _drawing;
|
||||
private readonly PartModeler _modeler;
|
||||
|
||||
public DrawingStampTests(KompasFixture fx) : base(fx)
|
||||
{
|
||||
_docs = new DocumentService(fx.Session, fx.Dispatcher);
|
||||
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
|
||||
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Fills_and_reads_back_title_block_cells()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
try
|
||||
{
|
||||
var filled = await _drawing.FillTitleBlockAsync(
|
||||
designation: "ДЕТАЛЬ.001", name: "Кронштейн", material: "Сталь 45");
|
||||
Assert.Equal(3, filled);
|
||||
|
||||
// Явный reread каждой графы (графа 2/1/3).
|
||||
Assert.Equal("ДЕТАЛЬ.001", await _drawing.ReadStampCellAsync(2));
|
||||
Assert.Equal("Кронштейн", await _drawing.ReadStampCellAsync(1));
|
||||
Assert.Equal("Сталь 45", await _drawing.ReadStampCellAsync(3));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Overwrites_existing_cell_text()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
try
|
||||
{
|
||||
await _drawing.FillTitleBlockAsync(designation: "СТАРОЕ.000");
|
||||
await _drawing.FillTitleBlockAsync(designation: "НОВОЕ.111");
|
||||
|
||||
// Перезапись, не дописывание.
|
||||
Assert.Equal("НОВОЕ.111", await _drawing.ReadStampCellAsync(2));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Empty_input_throws()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
try
|
||||
{
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
() => _drawing.FillTitleBlockAsync(null, " ", null));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rejects_non_drawing_document()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Деталь — не чертёж: FillTitleBlock должен бросить.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddRectangleAsync(s, 0, 0, 10, 10);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => _drawing.FillTitleBlockAsync(designation: "X"));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Kompas.Mcp.Core.Drawings;
|
||||
|
||||
namespace Kompas.Mcp.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class StampFieldsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(StampField.Designation, 2)]
|
||||
[InlineData(StampField.Name, 1)]
|
||||
[InlineData(StampField.Material, 3)]
|
||||
public void ColumnId_maps_known_fields(StampField field, int expected)
|
||||
=> Assert.Equal(expected, StampFields.ColumnId(field));
|
||||
|
||||
[Fact]
|
||||
public void ColumnId_throws_on_unknown()
|
||||
=> Assert.Throws<ArgumentOutOfRangeException>(() => StampFields.ColumnId((StampField)999));
|
||||
|
||||
[Fact]
|
||||
public void Collect_skips_null_empty_and_whitespace()
|
||||
=> Assert.Empty(StampFields.Collect(null, "", " "));
|
||||
|
||||
[Fact]
|
||||
public void Collect_trims_and_maps_provided_fields()
|
||||
{
|
||||
var result = StampFields.Collect(" ДЕТАЛЬ.001 ", null, "Сталь 45");
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains(result, t => t.ColumnId == 2 && t.Text == "ДЕТАЛЬ.001"); // designation
|
||||
Assert.Contains(result, t => t.ColumnId == 3 && t.Text == "Сталь 45"); // material
|
||||
Assert.DoesNotContain(result, t => t.ColumnId == 1); // name не задан
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Collect_includes_name_as_column_1()
|
||||
{
|
||||
var result = StampFields.Collect(null, "Кронштейн", null);
|
||||
Assert.Single(result);
|
||||
Assert.Equal((1, "Кронштейн"), result[0]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user