675e43160e
- ConicHoleAsync (ksHTConic + IConicHoleParameters: ConicType=ksCNAngle, ConicAngle) - инструмент hole_conic (diameter у дна, conicAngle 0..180); тот же HoleCore-паттерн - интеграционный тест (конус удаляет больше цилиндра Ø10); типы отверстий завершены - 70 инструментов, 119 тестов
204 lines
13 KiB
C#
204 lines
13 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|