using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using KompasAPI7;
namespace Kompas.Mcp.Core.Drawings;
///
/// Операции с 2D-чертежами через API7 (IViews). Все вызовы — на STA-потоке
/// .
///
[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));
}
///
/// Создать на активном чертеже стандартные ассоциативные виды (спереди/сверху/слева) сохранённой
/// 3D-модели (.m3d/.a3d). Главный вид привязывается к точке
/// (,) листа, мм; масштаб — .
///
public Task CreateStandardViewsAsync(string partFilePath, double scale,
double x, double y, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => CreateStandardViewsCore(partFilePath, scale, x, y), ct);
///
/// Заполнить графы основной надписи активного чертежа: обозначение (графа 2), наименование
/// изделия (графа 1), материал (графа 3). Передаются только нужные поля (null/пусто —
/// пропускается). Возвращает число заполненных граф.
///
public Task 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;
}
/// Прочитать текст графы основной надписи (для проверки/диагностики).
public Task ReadStampCellAsync(int columnId, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var stamp = RequireActiveStamp();
return RequireStampCell(stamp, columnId).Str ?? string.Empty;
}, ct);
/// Получить графу основной надписи по номеру с диагностикой (номер > 0, ячейка != null).
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).");
}
/// Лист оформления чертежа по номеру (1-based) с диагностикой (номер > 0, лист существует).
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}.");
}
///
/// Задать формат (: A0–A5 или user) и ориентацию листа
/// активного чертежа. Для стандартного формата размеры авто
/// (/ запрещены); для user — заданные width/height (мм),
/// при этом игнорируется (ориентация из соотношения сторон). Возвращает
/// итоговый формат, размеры и ориентацию (read-back из COM).
///
public Task 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,
};
}
///
/// Поставить линейный размер на виде активного чертежа между точками (x1,y1)-(x2,y2) в ЛОКАЛЬНОЙ
/// СК вида (мм); размерная линия проходит через (x3,y3). — номер вида
/// (≤0 = первый несистемный/главный). Значение измеряется автоматически. Возвращает значение и номер вида.
///
public Task 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;
}
}
/// Число линейных размеров на виде по номеру (для проверки/диагностики). Размеры живут
/// в ISymbols2DContainer вида, а НЕ в IView.ObjectCount (тот считает геометрию).
public Task GetViewLineDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.LineDimensions?.Count ?? 0, ct);
///
/// Поставить диаметральный размер (Ø) окружности на виде. — направление
/// выноски (градусы). При =false — свободный размер по (xc,yc,radius),
/// значение = 2·radius. При =true — (xc,yc,radius) служат ключом поиска
/// спроецированной окружности (Circles), размер привязывается к ней (BaseObject), значение Ø читается
/// из геометрии. Координаты — ЛОКАЛЬНАЯ СК вида (мм). Возвращает диаметр и номер вида.
///
public Task 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;
}
}
/// Число диаметральных размеров на виде (для проверки попадания в вид — они в
/// ISymbols2DContainer, а не в IView.ObjectCount).
public Task GetViewDiametralDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.DiametralDimensions?.Count ?? 0, ct);
///
/// Поставить радиальный размер (R) окружности на виде. — направление
/// выноски (градусы). При =false — свободный размер по (xc,yc,radius),
/// значение = radius. При =true — (xc,yc,radius) служат ключом поиска
/// спроецированной окружности (Circles), размер привязывается к ней (BaseObject), значение R читается
/// из геометрии. Координаты — ЛОКАЛЬНАЯ СК вида (мм). Возвращает радиус и номер вида.
///
public Task 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;
}
}
/// Число радиальных размеров на виде (для проверки попадания в вид — они в
/// ISymbols2DContainer, а не в IView.ObjectCount).
public Task GetViewRadialDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.RadialDimensions?.Count ?? 0, ct);
///
/// Поставить угловой размер между лучами (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) (ЛОКАЛЬНАЯ СК вида, мм)
/// на виде. (x3,y3) — положение размерной дуги (задаёт её радиус = расстояние от вершины).
/// выбирает, какой из двух углов измерять (min/max/more) — например
/// 90° или 270° для перпендикуляра. Значение (градусы) измеряется автоматически. Возвращает угол (°) и номер вида.
///
public Task 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;
}
}
/// Число угловых размеров на виде (для проверки попадания в вид — они в
/// ISymbols2DContainer, а не в IView.ObjectCount).
public Task GetViewAngularDimensionCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.AngleDimensions?.Count ?? 0, ct);
// ───────────────────────── Текстовые обозначения (инкремент 6) ─────────────────────────
///
/// Поставить знак шероховатости на виде в точке (,)
/// (ЛОКАЛЬНАЯ СК вида, мм). — текст параметра (Ra/Rz; пусто = знак без
/// значения). — тип знака. — угол наклона
/// оси знака (°). Возвращает значение (read-back) и номер вида.
///
public Task 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;
}
}
/// Число знаков шероховатости на виде (для проверки попадания в вид).
public Task GetViewRoughCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.Roughs?.Count ?? 0, ct);
///
/// Поставить свободную текстовую надпись на виде в точке (,)
/// (ЛОКАЛЬНАЯ СК вида, мм) стилем по умолчанию. — содержимое (\n — многострочно).
/// — угол наклона текста (°). Возвращает текст (read-back) и номер вида.
///
public Task 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;
}
}
/// Число текстовых надписей на виде (для проверки попадания в вид; текст в
/// IDrawingContainer, а не в IView.ObjectCount).
public Task GetViewTextCountAsync(int viewNumber, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => RequireDrawingContainer(viewNumber).Container.DrawingTexts?.Count ?? 0, ct);
///
/// Задать технические требования активного чертежа (единый блок над основной надписью; перезаписывает
/// прежние). — строки через \n. Возвращает число строк.
///
public Task 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;
}
/// Прочитать текст технических требований активного чертежа (для проверки/диагностики).
public Task 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).");
}
/// Вид + его IDrawingContainer (QI от вида). Для текстовых надписей (DrawingTexts).
private (IView View, IDrawingContainer Container) RequireDrawingContainer(int viewNumber)
{
var view = FindView(viewNumber);
var container = view as IDrawingContainer
?? throw new InvalidOperationException("Вид не приводится к IDrawingContainer.");
return (view, container);
}
/// Допуск совпадения окружности с ключом поиска при ассоциативной привязке (мм).
private const double CircleMatchToleranceMm = 1.0;
/// Вид + оба контейнера (QI от одного вида): ISymbols2DContainer (создание размера) и
/// IDrawingContainer (поиск геометрии для ассоциативной привязки).
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);
}
/// Найти окружность вида по ключу (центр + радиус) и вернуть её как IDrawingObject
/// (для dim.BaseObject). Бросает при отсутствии/неоднозначности.
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;
}
/// Собрать окружности вида (IDrawingContainer.Circles) с их геометрией и COM-ссылкой.
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;
}
/// Вид + его ISymbols2DContainer (QI от вида). Общий для линейных/диаметральных/… размеров.
private (IView View, ISymbols2DContainer Symbols) RequireSymbols2DContainer(int viewNumber)
{
var view = FindView(viewNumber);
var symbols = view as ISymbols2DContainer
?? throw new InvalidOperationException("Вид не приводится к ISymbols2DContainer.");
return (view, symbols);
}
/// Найти вид: при viewNumber ≤ 0 — первый несистемный (Number != 0); иначе по номеру.
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;
}
}
///
/// Получить активный чертёж как . Раздельные ошибки: нет документа /
/// тип != ksDocumentDrawing / не приводится к IKompasDocument2D.
///
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 CollectViewNumbers(IViews views)
{
var set = new HashSet();
foreach (var o in views)
if (o is IView v)
set.Add(v.Number);
return set;
}
private static List CollectNewViews(IViews views, HashSet beforeNumbers)
{
var list = new List();
foreach (var o in views)
if (o is IView v && !beforeNumbers.Contains(v.Number))
list.Add(v);
return list;
}
}