24f3531a3c
- 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>
689 lines
42 KiB
C#
689 lines
42 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();
|
||
// Нетранзакционно: все графы записываются, затем один 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>Получить графу основной надписи по номеру с диагностикой (номер > 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) с диагностикой (номер > 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"/>: A0–A5 или 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;
|
||
}
|
||
}
|