feat: отверстие (hole) через API7 — IHoles3D/IHole3D + IHoleDisposal
- HoleService (Core/Modeling): простое цилиндрическое отверстие (ksHTBase), сквозное/глухое - размещение: BaseSurface=грань (FindObjectsByPoint) + AssociationVertex=Points3D.Add() (точка по мировым координатам ksPParamCoord+X/Y/Z); эскиз размещения не нужен - в API5 определения отверстия НЕТ (как draft) — реализовано на API7 (прецедент: move_face) - авто-направление в тело с повтором; DI-регистрация; инструмент hole в FeatureTools - интеграционные тесты: сквозное (Ø10 удаляет π·25·20) + глухое (Ø8×10); spike зелёный с первого раза - 103 теста зелёных; спек docs/superpowers/specs/2026-05-27-hole-design.md
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
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), ct);
|
||||
|
||||
private void HoleCore(double x, double y, double z, double diameter, double depth, bool throughAll)
|
||||
{
|
||||
if (!(diameter > 0)) throw new ArgumentOutOfRangeException(nameof(diameter), "Диаметр должен быть > 0.");
|
||||
if (!throughAll && !(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 hole = container.Holes3D.Add()
|
||||
?? throw new InvalidOperationException("Holes3D.Add() вернул null.");
|
||||
hole.HoleType = ksHoleTypeEnum.ksHTBase;
|
||||
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;
|
||||
|
||||
var disp = (IHoleDisposal)hole;
|
||||
disp.BaseSurface = (IModelObject)face;
|
||||
disp.Perpendicular = true;
|
||||
disp.AssociationVertex = (IModelObject)pt;
|
||||
disp.Direction = true;
|
||||
|
||||
if (!hole.Update())
|
||||
{
|
||||
disp.Direction = false;
|
||||
if (!hole.Update())
|
||||
throw new InvalidOperationException("Update() отверстия вернул FALSE для обоих направлений.");
|
||||
}
|
||||
|
||||
doc3d.RebuildDocument();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ builder.Services.AddSingleton<KompasDispatcher>();
|
||||
builder.Services.AddSingleton<KompasSession>();
|
||||
builder.Services.AddSingleton<DocumentService>();
|
||||
builder.Services.AddSingleton<PartModeler>();
|
||||
builder.Services.AddSingleton<HoleService>();
|
||||
builder.Services.AddSingleton<SnapshotService>();
|
||||
builder.Services.AddSingleton<QueryService>();
|
||||
builder.Services.AddSingleton<ModelInspectionService>();
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Kompas.Mcp.Host.Tools;
|
||||
|
||||
/// <summary>Инструменты формообразующих операций.</summary>
|
||||
[McpServerToolType]
|
||||
public sealed class FeatureTools(KompasSession session, PartModeler modeler)
|
||||
public sealed class FeatureTools(KompasSession session, PartModeler modeler, HoleService holes)
|
||||
{
|
||||
[McpServerTool(Name = "extrude_boss")]
|
||||
[Description("Выдавить эскиз, добавив материал (на глубину depth, мм). Возвращает id операции.")]
|
||||
@@ -184,6 +184,20 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler)
|
||||
return $"Тело зеркально отражено (плоскость {plane}), id={id}.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "hole")]
|
||||
[Description("Просверлить простое цилиндрическое отверстие (API7) на грани, выбранной по мировой точке (x,y,z в мм — точка должна лежать на грани и задаёт ЦЕНТР отверстия; сделайте model_snapshot/describe_face и прикиньте точку). diameter — диаметр (мм). throughAll=true — сквозное (depth не нужен); иначе глухое на глубину depth (мм). Направление в тело подбирается автоматически. Отличие от extrude_cut: параметрическое отверстие без эскиза.")]
|
||||
public async Task<string> Hole(
|
||||
double x, double y, double z,
|
||||
[Description("Диаметр отверстия, мм")] double diameter,
|
||||
[Description("Глубина, мм (нужна, если throughAll=false)")] double depth = 0,
|
||||
[Description("Сквозное отверстие через всё тело")] bool throughAll = false)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await holes.HoleAsync(x, y, z, diameter, depth, throughAll);
|
||||
var kind = throughAll ? "сквозное" : $"глухое на {depth} мм";
|
||||
return $"Отверстие Ø{diameter} ({kind}) создано на грани в точке ({x}, {y}, {z}).";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "rebuild")]
|
||||
[Description("Перестроить активный документ.")]
|
||||
public async Task<string> Rebuild()
|
||||
|
||||
Reference in New Issue
Block a user