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