using Kompas.Mcp.Core.Documents; using Kompas.Mcp.Core.Modeling; using Kompas.Mcp.Core.Query; using Kompas.Mcp.Core.Vision; namespace Kompas.Mcp.Tests.Integration; /// Интеграция: построение 3D (эскиз → выдавливание) и снимок. [Trait("Category", "Integration")] [Collection(KompasCollection.Name)] public sealed class ModelingTests { private readonly DocumentService _docs; private readonly PartModeler _modeler; private readonly SnapshotService _snap; private readonly QueryService _query; public ModelingTests(KompasFixture fx) { _docs = new DocumentService(fx.Session, fx.Dispatcher); _modeler = new PartModeler(fx.Session, fx.Dispatcher); _snap = new SnapshotService(fx.Session, fx.Dispatcher); _query = new QueryService(fx.Session, fx.Dispatcher); } [Fact] public async Task Build_cylinder_then_cut_hole_and_snapshot() { await _docs.CreateAsync(KompasDocumentType.Part); try { // Эскиз 1: окружность R20 на XOY → выдавить boss на 40 (цилиндр). var s1 = await _modeler.OpenSketchAsync(BasePlane.XOY); await _modeler.AddCircleAsync(s1, 0, 0, 20); await _modeler.CloseSketchAsync(s1); var boss = await _modeler.ExtrudeAsync(s1, depth: 40); Assert.True(boss > 0); // Эскиз 2: окружность R8 на XOY → вырезать насквозь (отверстие). var s2 = await _modeler.OpenSketchAsync(BasePlane.XOY); await _modeler.AddCircleAsync(s2, 0, 0, 8); await _modeler.CloseSketchAsync(s2); var cut = await _modeler.ExtrudeAsync(s2, depth: 40, cut: true, throughAll: true); Assert.True(cut > 0); await _modeler.RebuildAsync(); var path = TestPaths.NewFile(".png"); var snap = await _snap.CaptureActiveModelAsync(saveToFile: path); Assert.True(snap.Bytes.Length > 1000, "Снимок цилиндра подозрительно мал."); Assert.True(File.Exists(path)); } finally { await _docs.CloseAsync(save: false); } } [Fact] public async Task Sketch_on_face_adds_boss_on_top() { await _docs.CreateAsync(KompasDocumentType.Part); try { // Цилиндр R10×H20 на XOY → верхняя грань на Z=20. var s1 = await _modeler.OpenSketchAsync(BasePlane.XOY); await _modeler.AddCircleAsync(s1, 0, 0, 10); await _modeler.CloseSketchAsync(s1); await _modeler.ExtrudeAsync(s1, depth: 20); // Запрос ДО достройки — проверяем, что повторный запрос увидит изменения // (МЦХ не должна «залипнуть» на устаревшем значении). var before = await _query.GetPartInfoAsync(); // Эскиз на верхней грани (точка (0,0,20)) → штифт R5×H10 наружу. var s2 = await _modeler.OpenSketchOnFaceAsync(0, 0, 20); await _modeler.AddCircleAsync(s2, 0, 0, 5); await _modeler.CloseSketchAsync(s2); var boss = await _modeler.ExtrudeAsync(s2, depth: 10); Assert.True(boss > 0); await _modeler.RebuildAsync(); var after = await _query.GetPartInfoAsync(); // Прирост объёма ≈ объём штифта π·5²·10 ≈ 785.4 мм³. var deltaExpected = Math.PI * 25 * 10; Assert.InRange(after.Volume - before.Volume, deltaExpected * 0.9, deltaExpected * 1.1); } finally { await _docs.CloseAsync(save: false); } } [Fact] public async Task Revolve_rectangle_makes_tube() { await _docs.CreateAsync(KompasDocumentType.Part); try { // Профиль-прямоугольник [10..15]×[0..20] + осевая по локальной оси Y (x=0). // Вращение 360° вокруг оси → труба: внеш. R15, внутр. R10, высота 20. var s = await _modeler.OpenSketchAsync(BasePlane.XOY); await _modeler.AddAxisAsync(s, 0, 0, 0, 20); await _modeler.AddRectangleAsync(s, 10, 0, 15, 20); await _modeler.CloseSketchAsync(s); var rev = await _modeler.RevolveAsync(s, angle: 360); Assert.True(rev > 0); await _modeler.RebuildAsync(); var info = await _query.GetPartInfoAsync(); // Объём трубы = π·(15² − 10²)·20 = π·2500 ≈ 7853.98 мм³. var expected = Math.PI * (15 * 15 - 10 * 10) * 20; Assert.InRange(info.Volume, expected * 0.97, expected * 1.03); } finally { await _docs.CloseAsync(save: false); } } [Fact] public async Task Fillet_and_chamfer_edges() { await _docs.CreateAsync(KompasDocumentType.Part); try { // Цилиндр R10×H20 → верхнее ребро на (10,0,20), нижнее на (10,0,0). var s = await _modeler.OpenSketchAsync(BasePlane.XOY); await _modeler.AddCircleAsync(s, 0, 0, 10); await _modeler.CloseSketchAsync(s); await _modeler.ExtrudeAsync(s, depth: 20); var cylinderVolume = (await _query.GetPartInfoAsync()).Volume; // Скругление верхнего ребра R3 и фаска нижнего 2×2. var fil = await _modeler.FilletEdgeAsync(10, 0, 20, 3); Assert.True(fil > 0); var cha = await _modeler.ChamferEdgeAsync(10, 0, 0, 2); Assert.True(cha > 0); await _modeler.RebuildAsync(); var path = TestPaths.NewFile("_fillet.png"); var snap = await _snap.CaptureActiveModelAsync(saveToFile: path); Assert.True(snap.Bytes.Length > 1000); // Скругление и фаска снимают материал с рёбер → объём уменьшается, но незначительно. var after = (await _query.GetPartInfoAsync()).Volume; Assert.True(after < cylinderVolume, "После скругления/фаски объём должен уменьшиться."); Assert.InRange(after, cylinderVolume * 0.9, cylinderVolume); } finally { await _docs.CloseAsync(save: false); } } [Fact] public async Task Reset_invalidates_sketch_ids() { await _docs.CreateAsync(KompasDocumentType.Part); try { var s = await _modeler.OpenSketchAsync(BasePlane.XOY); await _modeler.ResetAsync(); // После сброса реестра старый id недействителен. await Assert.ThrowsAsync( () => _modeler.AddCircleAsync(s, 0, 0, 5)); } finally { await _docs.CloseAsync(save: false); } } }