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; } }