742a97177b
Веха 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>
161 lines
8.7 KiB
C#
161 lines
8.7 KiB
C#
using System.Runtime.Versioning;
|
|
using Kompas.Mcp.Core.Threading;
|
|
using Kompas6Constants;
|
|
using KompasAPI7;
|
|
|
|
namespace Kompas.Mcp.Core.Drawings;
|
|
|
|
/// <summary>
|
|
/// Операции с 2D-чертежами через API7 (<c>IViews</c>). Все вызовы — на STA-потоке
|
|
/// <see cref="KompasDispatcher"/>.
|
|
/// </summary>
|
|
[SupportedOSPlatform("windows")]
|
|
public sealed class DrawingService
|
|
{
|
|
private readonly KompasSession _session;
|
|
private readonly KompasDispatcher _dispatcher;
|
|
|
|
public DrawingService(KompasSession session, KompasDispatcher dispatcher)
|
|
{
|
|
_session = session ?? throw new ArgumentNullException(nameof(session));
|
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Создать на активном чертеже стандартные ассоциативные виды (спереди/сверху/слева) сохранённой
|
|
/// 3D-модели <paramref name="partFilePath"/> (.m3d/.a3d). Главный вид привязывается к точке
|
|
/// (<paramref name="x"/>,<paramref name="y"/>) листа, мм; масштаб — <paramref name="scale"/>.
|
|
/// </summary>
|
|
public Task<DrawingViewsResult> CreateStandardViewsAsync(string partFilePath, double scale,
|
|
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;
|
|
private const double GapMm = 20; // зазоры между видами (DX=DY)
|
|
|
|
private DrawingViewsResult CreateStandardViewsCore(string partFilePath, double scale, double x, double y)
|
|
{
|
|
var path = DrawingValidation.NormalizeModelPath(partFilePath);
|
|
DrawingValidation.RequireScale(scale);
|
|
if (!double.IsFinite(x) || !double.IsFinite(y))
|
|
throw new ArgumentException("Координаты привязки вида должны быть конечными числами.");
|
|
if (!File.Exists(path))
|
|
throw new FileNotFoundException($"Файл модели не найден: {path}", path);
|
|
if (new FileInfo(path).Length == 0)
|
|
throw new InvalidOperationException($"Файл модели пуст (не сохранён?): {path}");
|
|
|
|
var views = RequireActiveDrawingViews();
|
|
// Снимок номеров существующих видов — чтобы потом отличить созданные ИМЕННО этим вызовом
|
|
// (на чертеже уже есть системный вид и могут быть прежние виды).
|
|
var beforeNumbers = CollectViewNumbers(views);
|
|
|
|
if (!views.AddStandartViews(path, "#Спереди", StandardProjections, x, y, scale, GapMm, GapMm))
|
|
throw new InvalidOperationException(
|
|
$"AddStandartViews вернул FALSE — виды не созданы (файл модели или проекция недоступны): {path}");
|
|
|
|
var newViews = CollectNewViews(views, beforeNumbers);
|
|
try
|
|
{
|
|
if (newViews.Count != ExpectedCreated)
|
|
throw new InvalidOperationException(
|
|
$"Ожидалось {ExpectedCreated} вида, создано {newViews.Count}.");
|
|
|
|
// Гарантия непустоты: у созданных видов должна быть геометрия (иначе модель не
|
|
// спроецировалась). Проверяем ИМЕННО новые виды, не всю коллекцию.
|
|
if (newViews.Sum(v => v.ObjectCount) <= 0)
|
|
throw new InvalidOperationException(
|
|
"Стандартные виды созданы, но пусты (модель не спроецировалась). Проверьте файл модели.");
|
|
|
|
return new DrawingViewsResult { Created = newViews.Count, Total = views.Count };
|
|
}
|
|
catch
|
|
{
|
|
// Откат: удаляем виды, созданные этим вызовом, чтобы чертёж не остался в полу-состоянии.
|
|
foreach (var v in newViews)
|
|
try { v.Delete(); } catch { /* лучшее усилие */ }
|
|
throw;
|
|
}
|
|
}
|
|
|
|
/// <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.");
|
|
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>();
|
|
foreach (var o in views)
|
|
if (o is IView v)
|
|
set.Add(v.Number);
|
|
return set;
|
|
}
|
|
|
|
private static List<IView> CollectNewViews(IViews views, HashSet<int> beforeNumbers)
|
|
{
|
|
var list = new List<IView>();
|
|
foreach (var o in views)
|
|
if (o is IView v && !beforeNumbers.Contains(v.Number))
|
|
list.Add(v);
|
|
return list;
|
|
}
|
|
}
|