using System.Runtime.Versioning; using Kompas.Mcp.Core.Threading; using Kompas6API5; using Kompas6Constants3D; using KompasAPI7; namespace Kompas.Mcp.Core.Modeling; /// /// Операция «Отверстие» через API7 (IHoles3D/IHole3D). В API5 определения /// отверстия нет, поэтому используется современный API7 (как и прямое редактирование грани). /// [SupportedOSPlatform("windows")] public sealed class HoleService { private readonly KompasSession _session; private readonly KompasDispatcher _dispatcher; public HoleService(KompasSession session, KompasDispatcher dispatcher) { _session = session ?? throw new ArgumentNullException(nameof(session)); _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); } /// /// Просверлить простое цилиндрическое отверстие (ksHTBase) на грани, найденной по мировой точке /// (,,), с центром в этой точке. /// diameter — диаметр (мм). throughAll — сквозное (depth игнорируется), иначе на глубину depth (мм). /// public Task HoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTBase, null), ct); /// Отверстие с цековкой (цилиндрическим уширением сверху, под винт с цилиндрической /// головкой): основное отверстие diameter + цековка spotDiameter×spotDepth. public Task CounterboreHoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll, double spotDiameter, double spotDepth, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { if (!double.IsFinite(spotDiameter) || spotDiameter <= diameter) throw new ArgumentOutOfRangeException(nameof(spotDiameter), "Диаметр цековки должен быть больше диаметра отверстия."); if (!double.IsFinite(spotDepth) || spotDepth <= 0) throw new ArgumentOutOfRangeException(nameof(spotDepth), "Глубина цековки должна быть > 0."); if (!throughAll && double.IsFinite(depth) && depth <= spotDepth) throw new ArgumentOutOfRangeException(nameof(depth), "Глубина глухого отверстия должна быть больше глубины цековки."); HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTCounterbore, hole => { var p = hole.HoleParameters as ISpotfacingHoleParameters ?? throw new InvalidOperationException("HoleParameters не приводится к ISpotfacingHoleParameters."); p.SpotfacingDiameter = spotDiameter; p.SpotfacingDepth = spotDepth; }); }, ct); /// Отверстие с зенковкой (коническим уширением сверху, под винт с потайной головкой): /// основное отверстие diameter + зенковка sinkDiameter под углом sinkAngle (градусы, конус). public Task CountersinkHoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll, double sinkDiameter, double sinkAngle, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { if (!double.IsFinite(sinkDiameter) || sinkDiameter <= diameter) throw new ArgumentOutOfRangeException(nameof(sinkDiameter), "Диаметр зенковки должен быть больше диаметра отверстия."); if (!double.IsFinite(sinkAngle) || sinkAngle <= 0 || sinkAngle >= 180) throw new ArgumentOutOfRangeException(nameof(sinkAngle), "Угол зенковки должен быть в (0; 180) градусов."); HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTCountersinking, hole => { var p = hole.HoleParameters as ICountersinkHoleParameters ?? throw new InvalidOperationException("HoleParameters не приводится к ICountersinkHoleParameters."); p.CountersinkType = ksCountersinkTypeEnum.ksCTDiameterAngle; // задаём диаметр + угол p.CountersinkDiameter = sinkDiameter; p.CountersinkAngle = sinkAngle; }); }, ct); /// Коническое отверстие (ksHTConic): конусность задаётся углом conicAngle (градусы). /// diameter — диаметр у дна, конус расширяется к поверхности. public Task ConicHoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll, double conicAngle, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { if (!double.IsFinite(conicAngle) || conicAngle <= 0 || conicAngle >= 180) throw new ArgumentOutOfRangeException(nameof(conicAngle), "Угол конуса должен быть в (0; 180) градусов."); HoleCore(x, y, z, diameter, depth, throughAll, ksHoleTypeEnum.ksHTConic, hole => { var p = hole.HoleParameters as IConicHoleParameters ?? throw new InvalidOperationException("HoleParameters не приводится к IConicHoleParameters."); p.ConicType = ksConicTypeEnum.ksCNAngle; p.ConicAngle = conicAngle; }); }, ct); private void HoleCore(double x, double y, double z, double diameter, double depth, bool throughAll, ksHoleTypeEnum holeType, Action? configure) { if (!double.IsFinite(x) || !double.IsFinite(y) || !double.IsFinite(z)) throw new ArgumentException("Координаты точки отверстия должны быть конечными числами."); if (!double.IsFinite(diameter) || diameter <= 0) throw new ArgumentOutOfRangeException(nameof(diameter), "Диаметр должен быть конечным и > 0."); if (!throughAll && (!double.IsFinite(depth) || depth <= 0)) throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть конечной и > 0 (или throughAll)."); if (_session.Application.ActiveDocument is not IKompasDocument3D doc3d) throw new InvalidOperationException("Активный документ не является 3D-документом."); if (doc3d.TopPart is not IPart7 top) throw new InvalidOperationException("Не удалось получить TopPart активного документа."); var face = FindFaceAtPoint(top, x, y, z) ?? throw new InvalidOperationException( $"В точке ({x}, {y}, {z}) не найдена грань. Сверьте координаты точки на грани (model_snapshot)."); var container = top as IModelContainer ?? throw new InvalidOperationException("Деталь не приводится к IModelContainer."); // Точка размещения центра отверстия в мировых координатах. var pt = container.Points3D.Add() ?? throw new InvalidOperationException("Points3D.Add() вернул null."); pt.ParameterType = ksPoint3DTypeEnum.ksPParamCoord; pt.X = x; pt.Y = y; pt.Z = z; if (!pt.Update()) throw new InvalidOperationException("Update() точки размещения вернул FALSE."); var before = CurrentVolume(); // Если отверстие не построится — удаляем уже созданную точку (и сам объект отверстия), // чтобы не засорять дерево «сиротами» при неуспехе. IHole3D? hole = null; try { hole = container.Holes3D.Add() ?? throw new InvalidOperationException("Holes3D.Add() вернул null."); hole.HoleType = holeType; hole.Diameter = diameter; hole.DepthType = throughAll ? ksDepthTypeEnum.ksDTReachThrough : ksDepthTypeEnum.ksDTValue; if (!throughAll) hole.Depth = depth; hole.EndFaceType = ksEndFaceTypeEnum.ksEFFlat; hole.Axis = false; hole.ShowThread = false; configure?.Invoke(hole); // параметры цековки/зенковки (после установки HoleType) var disp = (IHoleDisposal)hole; disp.BaseSurface = (IModelObject)face; disp.Perpendicular = true; disp.AssociationVertex = (IModelObject)pt; // Направление сверления подбираем ПО ФАКТУ удаления материала: нормаль грани может // смотреть в любую сторону, а при сквозном отверстии Update() возвращает TRUE даже когда // сверлит «в воздух» (наружу тела) — поэтому одного флага успеха Update недостаточно. if (!TryDirection(hole, disp, doc3d, true, before) && !TryDirection(hole, disp, doc3d, false, before)) throw new InvalidOperationException( "Отверстие не удалило материал ни в одном направлении (точка вне тела или диаметр/глубина несовместимы?)."); } catch { // Best-effort откат: сначала объект отверстия, затем точка размещения. try { hole?.Owner?.Delete(); } catch { /* лучшее усилие */ } try { pt.Owner?.Delete(); } catch { /* лучшее усилие */ } doc3d.RebuildDocument(); throw; } } /// Построить отверстие в заданном направлении и подтвердить, что материал убыл /// (объём уменьшился). Возвращает true только при фактическом удалении материала. private bool TryDirection(IHole3D hole, IHoleDisposal disp, IKompasDocument3D doc3d, bool direction, double before) { disp.Direction = direction; if (!hole.Update()) return false; doc3d.RebuildDocument(); return CurrentVolume() < before - 1e-6; } /// Текущий объём детали (мм³) через API5 МЦХ — согласован с геометрией API7 после /// RebuildDocument (как в QueryService). private double CurrentVolume() { const int mixMmKg = 0x1 | 0x10; // ST_MIX_MM | ST_MIX_KG var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D ?? throw new InvalidOperationException("Нет активного 3D-документа (API5)."); var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart ?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart)."); var mass = part.CalcMassInertiaProperties(mixMmKg) as ksMassInertiaParam ?? throw new InvalidOperationException("CalcMassInertiaProperties вернул null."); return mass.v; } private static IFace? FindFaceAtPoint(IPart7 part, double x, double y, double z) { var hit = part.FindObjectsByPoint(x, y, z, true); switch (hit) { case IFace f: return f; case object[] arr: foreach (var o in arr) if (o is IFace face) return face; return null; case System.Collections.IEnumerable en: foreach (var o in en) if (o is IFace face) return face; return null; default: return null; } } }