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:
2026-05-27 09:53:54 +03:00
parent 56f4a91364
commit 13ab0f6581
5 changed files with 269 additions and 1 deletions
@@ -0,0 +1,74 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Query;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: операция «Отверстие» (API7 IHoles3D/IHole3D).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class HoleTests
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly HoleService _holes;
private readonly QueryService _query;
public HoleTests(KompasFixture fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
_holes = new HoleService(fx.Session, fx.Dispatcher);
_query = new QueryService(fx.Session, fx.Dispatcher);
}
[Fact]
public async Task Hole_through_top_face_removes_cylinder_volume()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Коробка 40×40×20 (центр в начале координат), верхняя грань на Z=20.
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddRectangleAsync(s, -20, -20, 20, 20);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 20);
var before = (await _query.GetPartInfoAsync()).Volume; // 32000
// Сквозное отверстие Ø10 по центру верхней грани (0,0,20).
await _holes.HoleAsync(0, 0, 20, diameter: 10, depth: 0, throughAll: true);
var after = (await _query.GetPartInfoAsync()).Volume;
// Удалён цилиндр π·25·20 ≈ 1570.8 мм³.
var removed = Math.PI * 25 * 20;
Assert.True(after < before, "После отверстия объём должен уменьшиться.");
Assert.InRange(before - after, removed * 0.95, removed * 1.05);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Hole_blind_in_top_face_removes_partial_cylinder()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Коробка 40×40×20, верхняя грань на Z=20.
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddRectangleAsync(s, -20, -20, 20, 20);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 20);
var before = (await _query.GetPartInfoAsync()).Volume; // 32000
// Глухое отверстие Ø8 глубиной 10 по центру верхней грани.
await _holes.HoleAsync(0, 0, 20, diameter: 8, depth: 10, throughAll: false);
var after = (await _query.GetPartInfoAsync()).Volume;
// Удалён цилиндр π·16·10 ≈ 502.7 мм³ (без учёта плоского торца — EndFaceType=Flat).
var removed = Math.PI * 16 * 10;
Assert.True(after < before, "После глухого отверстия объём должен уменьшиться.");
Assert.InRange(before - after, removed * 0.9, removed * 1.1);
}
finally { await _docs.CloseAsync(save: false); }
}
}