Files
kompas3d-mcp/src/Kompas.Mcp.Core/Modeling/HoleService.cs
T
mikhail 675e43160e feat: коническое отверстие (hole_conic) — ksHTConic + IConicHoleParameters
- ConicHoleAsync (ksHTConic + IConicHoleParameters: ConicType=ksCNAngle, ConicAngle)
- инструмент hole_conic (diameter у дна, conicAngle 0..180); тот же HoleCore-паттерн
- интеграционный тест (конус удаляет больше цилиндра Ø10); типы отверстий завершены
- 70 инструментов, 119 тестов
2026-05-27 12:17:28 +03:00

204 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using Kompas6Constants3D;
using KompasAPI7;
namespace Kompas.Mcp.Core.Modeling;
/// <summary>
/// Операция «Отверстие» через API7 (<c>IHoles3D</c>/<c>IHole3D</c>). В API5 определения
/// отверстия нет, поэтому используется современный API7 (как и прямое редактирование грани).
/// </summary>
[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));
}
/// <summary>
/// Просверлить простое цилиндрическое отверстие (ksHTBase) на грани, найденной по мировой точке
/// (<paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>), с центром в этой точке.
/// diameter — диаметр (мм). throughAll — сквозное (depth игнорируется), иначе на глубину depth (мм).
/// </summary>
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);
/// <summary>Отверстие с цековкой (цилиндрическим уширением сверху, под винт с цилиндрической
/// головкой): основное отверстие diameter + цековка spotDiameter×spotDepth.</summary>
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);
/// <summary>Отверстие с зенковкой (коническим уширением сверху, под винт с потайной головкой):
/// основное отверстие diameter + зенковка sinkDiameter под углом sinkAngle (градусы, конус).</summary>
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);
/// <summary>Коническое отверстие (ksHTConic): конусность задаётся углом conicAngle (градусы).
/// diameter — диаметр у дна, конус расширяется к поверхности.</summary>
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<IHole3D>? 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;
}
}
/// <summary>Построить отверстие в заданном направлении и подтвердить, что материал убыл
/// (объём уменьшился). Возвращает true только при фактическом удалении материала.</summary>
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;
}
/// <summary>Текущий объём детали (мм³) через API5 МЦХ — согласован с геометрией API7 после
/// RebuildDocument (как в QueryService).</summary>
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;
}
}
}