Files
kompas3d-mcp/src/Kompas.Mcp.Core/Drawings/DrawingService.cs
T
mikhail 24f3531a3c review(drawing): правки по ревью реализации (Codex + pi/glm-5.1 + pi/kimi-k2.6)
- K1 (kimi, Major): спайк подтвердил — для User КОМПАС САМ выводит VerticalOrientation
  из W/H (игнорирует флаг, W/H не свопает); read-back корректен, правка не нужна —
  задокументировано комментарием + тесты на Landscape (500×300→альбомная, 300×500→книжная)
- K2: ValidateFormatDimensions называет точный нарушивший параметр (width/height)
- тесты: A1 в Parse (C3), A4 landscape со свопом 297×210 (G2), sheetNumber=0 (C2/G3)
- отклонено (нит): алиасы в описании (G4), сообщение NaN (G5), тип исключения standard (K5)

308 тестов зелёных, сборка Release чистая.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 23:28:34 +03:00

689 lines
42 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
// Нетранзакционно: все графы записываются, затем один Update() фиксирует. При Update()==FALSE
// (на практике не воспроизводится) откат старых значений не делаем — он спекулятивен (Str уже
// на in-memory объектах графы, повторный Update мог бы тоже упасть); сообщаем об ошибке.
foreach (var (columnId, text) in fields)
RequireStampCell(stamp, 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 RequireStampCell(stamp, columnId).Str ?? string.Empty;
}, ct);
/// <summary>Получить графу основной надписи по номеру с диагностикой (номер &gt; 0, ячейка != null).</summary>
private static IText RequireStampCell(IStamp stamp, int columnId)
{
if (columnId <= 0)
throw new ArgumentOutOfRangeException(nameof(columnId), "Номер графы основной надписи должен быть > 0.");
return stamp.Text[columnId]
?? throw new InvalidOperationException($"Графа {columnId} основной надписи недоступна (Text[{columnId}] == null).");
}
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).");
}
/// <summary>Лист оформления чертежа по номеру (1-based) с диагностикой (номер &gt; 0, лист существует).</summary>
private ILayoutSheet RequireLayoutSheet(int sheetNumber)
{
if (sheetNumber <= 0)
throw new ArgumentOutOfRangeException(nameof(sheetNumber), "Номер листа должен быть > 0.");
var doc2d = RequireActiveDrawing();
var sheets = doc2d.LayoutSheets
?? throw new InvalidOperationException("Не удалось получить листы оформления чертежа.");
return sheets.ItemByNumber[sheetNumber] as ILayoutSheet
?? throw new InvalidOperationException($"На чертеже нет листа оформления #{sheetNumber}.");
}
/// <summary>
/// Задать формат (<paramref name="format"/>: A0A5 или user) и ориентацию листа
/// <paramref name="sheetNumber"/> активного чертежа. Для стандартного формата размеры авто
/// (<paramref name="width"/>/<paramref name="height"/> запрещены); для user — заданные width/height (мм),
/// при этом <paramref name="landscape"/> игнорируется (ориентация из соотношения сторон). Возвращает
/// итоговый формат, размеры и ориентацию (read-back из COM).
/// </summary>
public Task<SheetFormatResult> SetSheetFormatAsync(PaperFormat format, bool landscape, double width,
double height, int sheetNumber = 1, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => SetSheetFormatCore(format, landscape, width, height, sheetNumber), ct);
private SheetFormatResult SetSheetFormatCore(PaperFormat format, bool landscape, double width,
double height, int sheetNumber)
{
DrawingValidation.ValidateFormatDimensions(format, width, height);
var sheet = RequireLayoutSheet(sheetNumber);
var fmt = sheet.Format
?? throw new InvalidOperationException("У листа недоступен формат (Format == null).");
fmt.Format = PaperFormats.ToKompas(format);
if (format == PaperFormat.User)
{
// Для пользовательского КОМПАС САМ выводит VerticalOrientation из соотношения width/height
// (W>H → альбомная, H>W → книжная) и игнорирует заданный флаг, W/H не свопает — проверено
// спайком. Поэтому VerticalOrientation не трогаем; read-back ниже даёт верную ориентацию.
fmt.FormatWidth = width;
fmt.FormatHeight = height;
}
else
{
fmt.VerticalOrientation = !landscape; // стандартный: размеры авто по формату+ориентации
}
// FormatMultiplicity оставляем на COM-дефолте (1).
if (!sheet.Update())
throw new InvalidOperationException("ILayoutSheet.Update вернул FALSE — формат листа не задан.");
var fmt2 = sheet.Format
?? throw new InvalidOperationException("Формат листа недоступен после Update (Format == null).");
return new SheetFormatResult
{
Format = PaperFormats.FromKompas(fmt2.Format).ToString(),
Width = fmt2.FormatWidth,
Height = fmt2.FormatHeight,
Landscape = !fmt2.VerticalOrientation,
};
}
/// <summary>
/// Поставить линейный размер на виде активного чертежа между точками (x1,y1)-(x2,y2) в ЛОКАЛЬНОЙ
/// СК вида (мм); размерная линия проходит через (x3,y3). <paramref name="viewNumber"/> — номер вида
/// (≤0 = первый несистемный/главный). Значение измеряется автоматически. Возвращает значение и номер вида.
/// </summary>
public Task<DrawingDimensionResult> AddLinearDimensionAsync(int viewNumber, double x1, double y1,
double x2, double y2, double x3, double y3, DimensionOrientation orientation, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
AddLinearDimensionCore(viewNumber, x1, y1, x2, y2, x3, y3, orientation), ct);
private DrawingDimensionResult AddLinearDimensionCore(int viewNumber, double x1, double y1,
double x2, double y2, double x3, double y3, DimensionOrientation orientation)
{
DrawingValidation.RequireFiniteCoords(x1, y1, x2, y2, x3, y3);
DrawingValidation.RequireDistinctPoints(x1, y1, x2, y2);
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
var dim = symbols.LineDimensions?.Add()
?? throw new InvalidOperationException("LineDimensions.Add вернул null.");
try
{
dim.X1 = x1; dim.Y1 = y1; dim.X2 = x2; dim.Y2 = y2; dim.X3 = x3; dim.Y3 = y3;
dim.Orientation = DimensionOrientations.ToKompas(orientation);
((IDimensionText)dim).AutoNominalValue = true; // авто-измерение значения (по умолчанию, явно)
if (!dim.Update())
throw new InvalidOperationException("ILineDimension.Update вернул FALSE — размер не построен.");
if (!dim.Valid)
throw new InvalidOperationException("Размер построен невалидным (проверьте точки/ориентацию).");
// NominalValue читаем ТОЛЬКО после успешного Update и Valid.
var value = ((IDimensionText)dim).NominalValue;
// Нулевое значение — ориентация несовместима с точками (напр. horizontal для точек на одной
// вертикали: |X2-X1|=0). RequireDistinctPoints это не ловит — точки различны по Y.
if (Math.Abs(value) < 1e-6)
throw new InvalidOperationException(
"Размер измерил нулевое значение — выбранная ориентация несовместима с точками " +
"(например, horizontal для точек на одной вертикали; используйте vertical/parallel).");
return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
}
catch
{
try { dim.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>Число линейных размеров на виде по номеру (для проверки/диагностики). Размеры живут
/// в ISymbols2DContainer вида, а НЕ в IView.ObjectCount (тот считает геометрию).</summary>
public Task<int> GetViewLineDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.LineDimensions?.Count ?? 0, ct);
/// <summary>
/// Поставить диаметральный размер (Ø) окружности на виде. <paramref name="angleDeg"/> — направление
/// выноски (градусы). При <paramref name="associate"/>=false — свободный размер по (xc,yc,radius),
/// значение = 2·radius. При <paramref name="associate"/>=true — (xc,yc,radius) служат ключом поиска
/// спроецированной окружности (Circles), размер привязывается к ней (BaseObject), значение Ø читается
/// из геометрии. Координаты — ЛОКАЛЬНАЯ СК вида (мм). Возвращает диаметр и номер вида.
/// </summary>
public Task<DrawingDimensionResult> AddDiametralDimensionAsync(int viewNumber, double xc, double yc,
double radius, double angleDeg, bool associate = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddDiametralDimensionCore(viewNumber, xc, yc, radius, angleDeg, associate), ct);
private DrawingDimensionResult AddDiametralDimensionCore(int viewNumber, double xc, double yc,
double radius, double angleDeg, bool associate)
{
DrawingValidation.RequireFiniteCoords(xc, yc);
DrawingValidation.RequirePositiveRadius(radius);
var angleRad = DimensionAngles.ToRadians(angleDeg);
// Свободный путь берёт только ISymbols2DContainer (как раньше); IDrawingContainer (геометрия)
// нужен лишь для ассоциативной привязки — не расширяем поверхность отказа свободного пути.
IView view; ISymbols2DContainer symbols; IDrawingObject? baseObject = null;
if (associate)
{
var c = RequireViewContainers(viewNumber);
view = c.View; symbols = c.Symbols;
baseObject = FindBaseCircle(c.Drawing, xc, yc, radius);
}
else
{
var c = RequireSymbols2DContainer(viewNumber);
view = c.View; symbols = c.Symbols;
}
var dim = symbols.DiametralDimensions?.Add()
?? throw new InvalidOperationException("DiametralDimensions.Add вернул null.");
try
{
if (baseObject != null)
dim.BaseObject = baseObject; // ассоциативно: значение Ø читается из геометрии
else
{ dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; }
dim.Angle = angleRad;
// DimensionType (стиль размерной линии) не задаём — оставляем COM-дефолт (полярность bool
// не подтверждена; дефолт даёт валидный размер с верным значением — проверено спайком).
((IDimensionText)dim).AutoNominalValue = true;
if (!dim.Update())
throw new InvalidOperationException("IDiametralDimension.Update вернул FALSE — размер не построен.");
if (!dim.Valid)
throw new InvalidOperationException("Диаметральный размер построен невалидным.");
var value = ((IDimensionText)dim).NominalValue; // диаметр = 2·radius
if (value <= 0 || !double.IsFinite(value)) // инвариант диаметра: строго положительный
throw new InvalidOperationException($"Диаметральный размер измерил некорректное значение ({value}).");
return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
}
catch
{
try { dim.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>Число диаметральных размеров на виде (для проверки попадания в вид — они в
/// ISymbols2DContainer, а не в IView.ObjectCount).</summary>
public Task<int> GetViewDiametralDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.DiametralDimensions?.Count ?? 0, ct);
/// <summary>
/// Поставить радиальный размер (R) окружности на виде. <paramref name="angleDeg"/> — направление
/// выноски (градусы). При <paramref name="associate"/>=false — свободный размер по (xc,yc,radius),
/// значение = radius. При <paramref name="associate"/>=true — (xc,yc,radius) служат ключом поиска
/// спроецированной окружности (Circles), размер привязывается к ней (BaseObject), значение R читается
/// из геометрии. Координаты — ЛОКАЛЬНАЯ СК вида (мм). Возвращает радиус и номер вида.
/// </summary>
public Task<DrawingDimensionResult> AddRadialDimensionAsync(int viewNumber, double xc, double yc,
double radius, double angleDeg, bool associate = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddRadialDimensionCore(viewNumber, xc, yc, radius, angleDeg, associate), ct);
private DrawingDimensionResult AddRadialDimensionCore(int viewNumber, double xc, double yc,
double radius, double angleDeg, bool associate)
{
DrawingValidation.RequireFiniteCoords(xc, yc);
DrawingValidation.RequirePositiveRadius(radius);
var angleRad = DimensionAngles.ToRadians(angleDeg);
// Свободный путь берёт только ISymbols2DContainer (как раньше); IDrawingContainer (геометрия)
// нужен лишь для ассоциативной привязки — не расширяем поверхность отказа свободного пути.
IView view; ISymbols2DContainer symbols; IDrawingObject? baseObject = null;
if (associate)
{
var c = RequireViewContainers(viewNumber);
view = c.View; symbols = c.Symbols;
baseObject = FindBaseCircle(c.Drawing, xc, yc, radius);
}
else
{
var c = RequireSymbols2DContainer(viewNumber);
view = c.View; symbols = c.Symbols;
}
var dim = symbols.RadialDimensions?.Add()
?? throw new InvalidOperationException("RadialDimensions.Add вернул null.");
try
{
if (baseObject != null)
dim.BaseObject = baseObject; // ассоциативно: значение R читается из геометрии
else
{ dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; }
dim.Angle = angleRad;
dim.DimensionType = true; // стиль R «от центра» (на величину не влияет — спайк)
((IDimensionText)dim).AutoNominalValue = true;
if (!dim.Update())
throw new InvalidOperationException("IRadialDimension.Update вернул FALSE — размер не построен.");
if (!dim.Valid)
throw new InvalidOperationException("Радиальный размер построен невалидным.");
var value = ((IDimensionText)dim).NominalValue; // = радиус (не диаметр — спайк)
if (value <= 0 || !double.IsFinite(value)) // инвариант радиуса: строго положительный
throw new InvalidOperationException($"Радиальный размер измерил некорректное значение ({value}).");
return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
}
catch
{
try { dim.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>Число радиальных размеров на виде (для проверки попадания в вид — они в
/// ISymbols2DContainer, а не в IView.ObjectCount).</summary>
public Task<int> GetViewRadialDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.RadialDimensions?.Count ?? 0, ct);
/// <summary>
/// Поставить угловой размер между лучами (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) (ЛОКАЛЬНАЯ СК вида, мм)
/// на виде. (x3,y3) — положение размерной дуги (задаёт её радиус = расстояние от вершины).
/// <paramref name="angleType"/> выбирает, какой из двух углов измерять (min/max/more) — например
/// 90° или 270° для перпендикуляра. Значение (градусы) измеряется автоматически. Возвращает угол (°) и номер вида.
/// </summary>
public Task<DrawingDimensionResult> AddAngularDimensionAsync(int viewNumber, double xc, double yc,
double x1, double y1, double x2, double y2, double x3, double y3,
AngleDimensionType angleType, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
AddAngularDimensionCore(viewNumber, xc, yc, x1, y1, x2, y2, x3, y3, angleType), ct);
private DrawingDimensionResult AddAngularDimensionCore(int viewNumber, double xc, double yc,
double x1, double y1, double x2, double y2, double x3, double y3, AngleDimensionType angleType)
{
DrawingValidation.RequireFiniteCoords(xc, yc, x1, y1, x2, y2, x3, y3);
DrawingValidation.RequireDistinctPoints(xc, yc, x1, y1); // вершина ≠ точка стороны 1
DrawingValidation.RequireDistinctPoints(xc, yc, x2, y2); // вершина ≠ точка стороны 2
DrawingValidation.RequireDistinctPoints(xc, yc, x3, y3); // вершина ≠ положение дуги (R дуги > 0)
DrawingValidation.RequireDistinctPoints(x1, y1, x2, y2); // стороны не совпадают (одинаковые лучи)
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
var dim = symbols.AngleDimensions?.Add(DrawingObjectTypeEnum.ksDrADimension)
?? throw new InvalidOperationException("AngleDimensions.Add вернул null.");
try
{
dim.Xc = xc; dim.Yc = yc;
dim.X1 = x1; dim.Y1 = y1; dim.X2 = x2; dim.Y2 = y2; dim.X3 = x3; dim.Y3 = y3;
dim.DimensionType = AngleDimensionTypes.ToKompas(angleType);
((IDimensionText)dim).AutoNominalValue = true;
if (!dim.Update())
throw new InvalidOperationException("IAngleDimension.Update вернул FALSE — размер не построен.");
if (!dim.Valid)
throw new InvalidOperationException("Угловой размер построен невалидным.");
var value = ((IDimensionText)dim).NominalValue; // градусы
// Нулевой/некорректный угол — стороны коллинеарны (лучи сонаправлены → угол вырожден).
if (value <= 0 || !double.IsFinite(value))
throw new InvalidOperationException(
$"Угловой размер измерил некорректное значение ({value}°) — стороны коллинеарны " +
"(лучи (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) сонаправлены).");
return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
}
catch
{
try { dim.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>Число угловых размеров на виде (для проверки попадания в вид — они в
/// ISymbols2DContainer, а не в IView.ObjectCount).</summary>
public Task<int> GetViewAngularDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.AngleDimensions?.Count ?? 0, ct);
// ───────────────────────── Текстовые обозначения (инкремент 6) ─────────────────────────
/// <summary>
/// Поставить знак шероховатости на виде в точке (<paramref name="x"/>,<paramref name="y"/>)
/// (ЛОКАЛЬНАЯ СК вида, мм). <paramref name="value"/> — текст параметра (Ra/Rz; пусто = знак без
/// значения). <paramref name="signType"/> — тип знака. <paramref name="angleDeg"/> — угол наклона
/// оси знака (°). Возвращает значение (read-back) и номер вида.
/// </summary>
public Task<DrawingAnnotationResult> 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;
}
}
/// <summary>Число знаков шероховатости на виде (для проверки попадания в вид).</summary>
public Task<int> GetViewRoughCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.Roughs?.Count ?? 0, ct);
/// <summary>
/// Поставить свободную текстовую надпись на виде в точке (<paramref name="x"/>,<paramref name="y"/>)
/// (ЛОКАЛЬНАЯ СК вида, мм) стилем по умолчанию. <paramref name="text"/> — содержимое (\n — многострочно).
/// <paramref name="angleDeg"/> — угол наклона текста (°). Возвращает текст (read-back) и номер вида.
/// </summary>
public Task<DrawingAnnotationResult> 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;
}
}
/// <summary>Число текстовых надписей на виде (для проверки попадания в вид; текст в
/// IDrawingContainer, а не в IView.ObjectCount).</summary>
public Task<int> GetViewTextCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireDrawingContainer(viewNumber).Container.DrawingTexts?.Count ?? 0, ct);
/// <summary>
/// Задать технические требования активного чертежа (единый блок над основной надписью; перезаписывает
/// прежние). <paramref name="text"/> — строки через \n. Возвращает число строк.
/// </summary>
public Task<int> 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;
}
/// <summary>Прочитать текст технических требований активного чертежа (для проверки/диагностики).</summary>
public Task<string> 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).");
}
/// <summary>Вид + его IDrawingContainer (QI от вида). Для текстовых надписей (DrawingTexts).</summary>
private (IView View, IDrawingContainer Container) RequireDrawingContainer(int viewNumber)
{
var view = FindView(viewNumber);
var container = view as IDrawingContainer
?? throw new InvalidOperationException("Вид не приводится к IDrawingContainer.");
return (view, container);
}
/// <summary>Допуск совпадения окружности с ключом поиска при ассоциативной привязке (мм).</summary>
private const double CircleMatchToleranceMm = 1.0;
/// <summary>Вид + оба контейнера (QI от одного вида): ISymbols2DContainer (создание размера) и
/// IDrawingContainer (поиск геометрии для ассоциативной привязки).</summary>
private (IView View, ISymbols2DContainer Symbols, IDrawingContainer Drawing) RequireViewContainers(int viewNumber)
{
var view = FindView(viewNumber);
var symbols = view as ISymbols2DContainer
?? throw new InvalidOperationException("Вид не приводится к ISymbols2DContainer.");
var drawing = view as IDrawingContainer
?? throw new InvalidOperationException("Вид не приводится к IDrawingContainer.");
return (view, symbols, drawing);
}
/// <summary>Найти окружность вида по ключу (центр + радиус) и вернуть её как IDrawingObject
/// (для <c>dim.BaseObject</c>). Бросает при отсутствии/неоднозначности.</summary>
private static IDrawingObject FindBaseCircle(IDrawingContainer drawing, double xc, double yc, double radius)
{
var items = CollectCircles(drawing);
var index = CircularObjectMatch.SelectMatchIndex(
items.Select(i => i.Geom).ToList(), xc, yc, radius, CircleMatchToleranceMm);
return items[index].Obj;
}
/// <summary>Собрать окружности вида (IDrawingContainer.Circles) с их геометрией и COM-ссылкой.</summary>
private static List<(IDrawingObject Obj, CircleCandidate Geom)> CollectCircles(IDrawingContainer drawing)
{
var result = new List<(IDrawingObject, CircleCandidate)>();
var circles = drawing.Circles;
if (circles == null) return result;
for (var i = 0; i < circles.Count; i++)
{
if (circles[i] is not ICircle c) continue;
result.Add(((IDrawingObject)c, new CircleCandidate(c.Xc, c.Yc, c.Radius)));
}
return result;
}
/// <summary>Вид + его ISymbols2DContainer (QI от вида). Общий для линейных/диаметральных/… размеров.</summary>
private (IView View, ISymbols2DContainer Symbols) RequireSymbols2DContainer(int viewNumber)
{
var view = FindView(viewNumber);
var symbols = view as ISymbols2DContainer
?? throw new InvalidOperationException("Вид не приводится к ISymbols2DContainer.");
return (view, symbols);
}
/// <summary>Найти вид: при viewNumber ≤ 0 — первый несистемный (Number != 0); иначе по номеру.</summary>
private IView FindView(int viewNumber)
{
var views = RequireActiveDrawingViews();
foreach (var o in views)
{
if (o is not IView v) continue;
if (viewNumber <= 0)
{
if (v.Number != 0) return v; // первый несистемный
}
else if (v.Number == viewNumber)
{
return v;
}
}
throw new InvalidOperationException(viewNumber <= 0
? "На чертеже нет несистемного вида. Сначала создайте виды: drawing_create_standard_views."
: $"Вид с номером {viewNumber} не найден на чертеже.");
}
// Стандартная тройка проекций: 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,
ViewNumbers = newViews.Select(v => v.Number).ToArray(),
};
}
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;
}
}