Files
kompas3d-mcp/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs
T
mikhail 143877c6cc fix: правки draft по ревью Codex
- угол уклона вынесен в чистый SketchGeometry.RequireDraftAngle (0<angle<90, finite) + unit-тесты границ — P2
- описание инструмента уточнено: строго 0 < angle < 90 — P2
- интеграционный тест Draft_outward_expands_box — страхует маппинг направления (outward→!direction) — P2
- 115 тестов (69 unit + 46 integration)
2026-05-27 10:28:23 +03:00

378 lines
19 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 Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Query;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: формообразующие операции пакетов B и C (массивы, зеркало).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class FeatureOpsTests
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly QueryService _query;
public FeatureOpsTests(KompasFixture fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
_query = new QueryService(fx.Session, fx.Dispatcher);
}
[Fact]
public async Task Shell_box_hollows_with_open_cap()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Коробка 40×30×20.
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddRectangleAsync(s, 0, 0, 40, 30);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 20);
var before = (await _query.GetPartInfoAsync()).Volume; // 24000
// Удаляем один из двух плоских торцов (площадь 40×30=1200) → открытая оболочка.
// Какой именно торец (верх/низ) — для объёма не важно: результат симметричен.
var faces = await _query.ListFacesAsync();
var cap = faces.First(f => f.Type == "plane" && Math.Abs(f.Area - 1200) < 1);
var id = await _modeler.ShellAsync(new[] { cap.Index }, thickness: 2, outward: false);
Assert.True(id > 0);
await _modeler.RebuildAsync();
// Оболочка 2 мм с открытым торцом: 24000 36×26×18 = 7152 мм³.
var after = (await _query.GetPartInfoAsync()).Volume;
Assert.True(after < before, "После оболочки объём должен уменьшиться.");
Assert.InRange(after, 7152 * 0.95, 7152 * 1.05);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Rib_adds_material_in_corner_of_L_bracket()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Г-образный уголок: L-профиль на XOZ (замкнутая ломаная), выдавить по Y на 30.
// Площадь L = 40×5 + 5×35 = 375 → объём 375·30 = 11250.
var sBody = await _modeler.OpenSketchAsync(BasePlane.XOZ);
var lprofile = new (double x, double y)[] { (0, 0), (40, 0), (40, 5), (5, 5), (5, 40), (0, 40) };
await _modeler.AddPolylineAsync(sBody, lprofile, closed: true);
await _modeler.CloseSketchAsync(sBody);
await _modeler.ExtrudeAsync(sBody, depth: 30);
var before = (await _query.GetPartInfoAsync()).Volume;
Assert.InRange(before, 11250 * 0.95, 11250 * 1.05); // L-уголок 375·30
// Ребро-косынка: разомкнутый отрезок во внутреннем углу, на XOZ (Y=0).
// Контур висит в зазоре (не касается полок); side=left, несимметрично → толщина 5 мм.
var sRib = await _modeler.OpenSketchAsync(BasePlane.XOZ);
await _modeler.AddLineAsync(sRib, 25, 10, 10, 25); // висит в пустом углу, не касаясь полок
await _modeler.CloseSketchAsync(sRib);
var id = await _modeler.RibAsync(sRib, thickness: 5, side: "left", symmetric: false);
Assert.True(id > 0);
await _modeler.RebuildAsync();
// Косынка добавляет материал: объём растёт, но в разумных пределах.
var after = (await _query.GetPartInfoAsync()).Volume;
Assert.True(after > before, "После ребра жёсткости объём должен увеличиться.");
Assert.InRange(after, before, before * 1.3);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Sweep_circle_along_line_makes_cylinder()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Профиль: окружность R5 на XOY (центр 0,0).
var profile = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(profile, 0, 0, 5);
await _modeler.CloseSketchAsync(profile);
// Траектория: вертикальный отрезок (0,0)→(0,50) на XOZ (по +Z из центра профиля).
var path = await _modeler.OpenSketchAsync(BasePlane.XOZ);
await _modeler.AddLineAsync(path, 0, 0, 0, 50);
await _modeler.CloseSketchAsync(path);
var id = await _modeler.SweepAsync(profile, path);
Assert.True(id > 0);
await _modeler.RebuildAsync();
// Окружность R5 по прямой длиной 50 → цилиндр: π·25·50 ≈ 3927 мм³.
var v = (await _query.GetPartInfoAsync()).Volume;
var expected = Math.PI * 25 * 50;
Assert.InRange(v, expected * 0.95, expected * 1.05);
// Габарит подтверждает ось: цилиндр R5 вдоль Z → ≈10×10×50.
var box = await _query.GetBoundingBoxAsync();
Assert.InRange(box.SizeX, 10 * 0.95, 10 * 1.05);
Assert.InRange(box.SizeY, 10 * 0.95, 10 * 1.05);
Assert.InRange(box.SizeZ, 50 * 0.95, 50 * 1.05);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task OffsetPlane_sketch_extrudes_at_height()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Эскиз на XOY, смещённой на +20 по Z → цилиндр R5×H10 с основанием на Z≈20.
var s = await _modeler.OpenSketchOnOffsetPlaneAsync(BasePlane.XOY, offset: 20);
await _modeler.AddCircleAsync(s, 0, 0, 5);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 10);
await _modeler.RebuildAsync();
var box = await _query.GetBoundingBoxAsync();
Assert.InRange(box.MinZ, 19.5, 20.5); // основание поднято на 20 мм
var v = (await _query.GetPartInfoAsync()).Volume;
Assert.InRange(v, Math.PI * 25 * 10 * 0.95, Math.PI * 25 * 10 * 1.05);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task LinearPattern_replicates_cylinder_along_X()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Цилиндр R5×H10 в центре (объём π·25·10 ≈ 785.4 мм³).
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(s, 0, 0, 5);
await _modeler.CloseSketchAsync(s);
var cyl = await _modeler.ExtrudeAsync(s, depth: 10);
var one = (await _query.GetPartInfoAsync()).Volume;
// Линейный массив: 3 экземпляра вдоль X с шагом 30 (R5 → не перекрываются).
var id = await _modeler.LinearPatternAsync(new[] { cyl }, CoordinateAxis.X, count: 3, step: 30);
Assert.True(id > 0);
await _modeler.RebuildAsync();
// count=3 включает исходный → суммарный объём ≈ 3 цилиндра.
var v = (await _query.GetPartInfoAsync()).Volume;
Assert.InRange(v, one * 3 * 0.95, one * 3 * 1.05);
// Габарит вдоль X: копии в 0/30/60, R5 → от −5 до 65 → размер ≈70 (знак шага не важен).
var box = await _query.GetBoundingBoxAsync();
Assert.InRange(box.SizeX, 70 * 0.95, 70 * 1.05);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task CircularPattern_replicates_cylinder_around_Z()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Цилиндр R3×H10 со смещением: центр (20,0) (объём π·9·10 ≈ 282.7 мм³).
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(s, 20, 0, 3);
await _modeler.CloseSketchAsync(s);
var cyl = await _modeler.ExtrudeAsync(s, depth: 10);
var one = (await _query.GetPartInfoAsync()).Volume;
// Круговой массив вокруг Z: 3 экз. с шагом 90° → центры (20,0),(0,20),(-20,0).
var id = await _modeler.CircularPatternAsync(new[] { cyl }, CoordinateAxis.Z, count: 3, step: 90);
Assert.True(id > 0);
await _modeler.RebuildAsync();
var v = (await _query.GetPartInfoAsync()).Volume;
Assert.InRange(v, one * 3 * 0.95, one * 3 * 1.05);
// Габарит: X от -23 до 23 → 46; Y от -3 до 23 → 26 (поворот реально произошёл).
var box = await _query.GetBoundingBoxAsync();
Assert.InRange(box.SizeX, 46 * 0.95, 46 * 1.05);
Assert.InRange(box.SizeY, 26 * 0.95, 26 * 1.05);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task CircularPattern_reverse_flips_rotation_direction()
{
// count=2 (исходный + 1 копия на 90°): forward и reverse уводят копию в РАЗНЫЕ
// стороны по Y, поэтому MaxY габарита заметно различается (проводка def.inverce).
async Task<double> CopyMaxY(bool reverse)
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(s, 20, 0, 3); // цилиндр R3 центр (20,0)
await _modeler.CloseSketchAsync(s);
var cyl = await _modeler.ExtrudeAsync(s, depth: 10);
await _modeler.CircularPatternAsync(new[] { cyl }, CoordinateAxis.Z, count: 2, step: 90, reverse: reverse);
await _modeler.RebuildAsync();
return (await _query.GetBoundingBoxAsync()).MaxY;
}
finally { await _docs.CloseAsync(save: false); }
}
var fwd = await CopyMaxY(reverse: false);
var rev = await CopyMaxY(reverse: true);
// Одно направление: копия в (0,+20) → MaxY≈23; другое: в (0,20) → MaxY≈3. Разница ~20.
Assert.True(Math.Abs(fwd - rev) > 10, $"reverse не меняет направление вращения: fwd MaxY={fwd:F1}, rev MaxY={rev:F1}");
}
[Fact]
public async Task MirrorOperation_mirrors_two_features_across_YOZ()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Две отдельные операции (проверяем заполнение коллекции несколькими источниками):
// цилиндр R3 центр (20,0) и цилиндр R2 центр (10,0), оба H10.
var s1 = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(s1, 20, 0, 3);
await _modeler.CloseSketchAsync(s1);
var c1 = await _modeler.ExtrudeAsync(s1, depth: 10);
var s2 = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(s2, 10, 0, 2);
await _modeler.CloseSketchAsync(s2);
var c2 = await _modeler.ExtrudeAsync(s2, depth: 10);
var before = (await _query.GetPartInfoAsync()).Volume; // π·9·10 + π·4·10 ≈ 408.4
// Зеркало обеих операций относительно YOZ (x=0) → копии в (-20,0),(-10,0).
var id = await _modeler.MirrorOperationAsync(new[] { c1, c2 }, BasePlane.YOZ);
Assert.True(id > 0);
await _modeler.RebuildAsync();
var v = (await _query.GetPartInfoAsync()).Volume; // удвоение
Assert.InRange(v, before * 2 * 0.95, before * 2 * 1.05);
// Габарит X симметричен: от -23 до 23 → 46.
var box = await _query.GetBoundingBoxAsync();
Assert.InRange(box.SizeX, 46 * 0.95, 46 * 1.05);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task MirrorBody_makes_symmetric_part_across_YOZ()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Цилиндр R3×H10 центр (20,0) (объём ≈ 282.7 мм³).
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(s, 20, 0, 3);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 10);
var before = (await _query.GetPartInfoAsync()).Volume;
// Зеркально отразить тело относительно YOZ → оригинал + зеркало в (-20,0).
var id = await _modeler.MirrorBodyAsync(BasePlane.YOZ);
Assert.True(id > 0);
await _modeler.RebuildAsync();
var v = (await _query.GetPartInfoAsync()).Volume;
Assert.InRange(v, before * 2 * 0.95, before * 2 * 1.05);
var box = await _query.GetBoundingBoxAsync();
Assert.InRange(box.SizeX, 46 * 0.95, 46 * 1.05); // от -23 до 23
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Draft_tapers_box_sides_into_frustum()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Коробка 40×40×20 (центр 0,0; низ на Z=0, верх на 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
// 4 боковые грани (площадь 40×20=800) под уклон 10° внутрь относительно XOY (низ остаётся 40×40).
var faces = await _query.ListFacesAsync();
var sides = faces.Where(f => f.Type == "plane" && Math.Abs(f.Area - 800) < 1).Select(f => f.Index).ToArray();
Assert.Equal(4, sides.Length);
var id = await _modeler.DraftAsync(sides, BasePlane.XOY, angle: 10, outward: false);
Assert.True(id > 0);
await _modeler.RebuildAsync();
// Сужение к верху → усечённая пирамида. Верх ≈ 40−2·20·tan10° = 32.95; A2≈1085.5.
// V = (20/3)(1600 + 1085.5 + √(1600·1085.5)) ≈ 26689 мм³.
var after = (await _query.GetPartInfoAsync()).Volume;
var expected = (20.0 / 3) * (1600 + 1085.5 + Math.Sqrt(1600 * 1085.5));
Assert.True(after < before, "Сужающий уклон должен уменьшить объём.");
Assert.InRange(after, expected * 0.9, expected * 1.1);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Draft_outward_expands_box()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Та же коробка 40×40×20 (низ на Z=0).
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
// 4 боковые грани под уклон 10° НАРУЖУ (outward=true) → верх расширяется, объём растёт.
// Страхует маппинг направления (def.direction = !outward).
var faces = await _query.ListFacesAsync();
var sides = faces.Where(f => f.Type == "plane" && Math.Abs(f.Area - 800) < 1).Select(f => f.Index).ToArray();
var id = await _modeler.DraftAsync(sides, BasePlane.XOY, angle: 10, outward: true);
Assert.True(id > 0);
await _modeler.RebuildAsync();
// Верх ≈ 40 + 2·20·tan10° = 47.05; V = (20/3)(1600 + 47.05² + √(1600·47.05²)) ≈ 37970.
var after = (await _query.GetPartInfoAsync()).Volume;
var top = 40 + 2 * 20 * Math.Tan(10 * Math.PI / 180);
var expected = (20.0 / 3) * (1600 + top * top + Math.Sqrt(1600 * top * top));
Assert.True(after > before, "Расширяющий уклон должен увеличить объём (маппинг outward).");
Assert.InRange(after, expected * 0.9, expected * 1.1);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Loft_two_squares_makes_solid()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Два квадратных сечения 20×20 на XOY (Z=0) и на XOY+40 → призма 20×20×40.
var s1 = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddRectangleAsync(s1, -10, -10, 10, 10);
await _modeler.CloseSketchAsync(s1);
var s2 = await _modeler.OpenSketchOnOffsetPlaneAsync(BasePlane.XOY, offset: 40);
await _modeler.AddRectangleAsync(s2, -10, -10, 10, 10);
await _modeler.CloseSketchAsync(s2);
var id = await _modeler.LoftAsync(new[] { s1, s2 });
Assert.True(id > 0);
await _modeler.RebuildAsync();
var v = (await _query.GetPartInfoAsync()).Volume; // 20×20×40 = 16000
Assert.InRange(v, 16000 * 0.9, 16000 * 1.1);
// Габарит подтверждает соединение сечений по высоте: ≈20×20×40.
var box = await _query.GetBoundingBoxAsync();
Assert.InRange(box.SizeX, 20 * 0.95, 20 * 1.05);
Assert.InRange(box.SizeY, 20 * 0.95, 20 * 1.05);
Assert.InRange(box.SizeZ, 40 * 0.95, 40 * 1.05);
}
finally { await _docs.CloseAsync(save: false); }
}
}