diff --git a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs index e82139b..ffa5690 100644 --- a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs +++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs @@ -419,6 +419,67 @@ public sealed partial class PartModeler return id; }, ct); + /// Зеркальная копия операций featureIds относительно координатной плоскости plane. + /// Возвращает id операции. + public Task MirrorOperationAsync(IReadOnlyList featureIds, BasePlane plane, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + ArgumentNullException.ThrowIfNull(featureIds); + var ids = featureIds.Distinct().ToList(); + if (ids.Count == 0) throw new ArgumentException("Нужна хотя бы одна операция для зеркала.", nameof(featureIds)); + + // Собираем источники до мутации модели (проверка существования всех id). + var features = ids.Select(RequireFeature).ToList(); + + var part = GetTopPart(); + var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity + ?? throw new InvalidOperationException("Не удалось получить координатную плоскость."); + + var entity = part.NewEntity((short)Obj3dType.o3d_mirrorOperation) as ksEntity + ?? throw new InvalidOperationException("NewEntity(o3d_mirrorOperation) вернул null."); + var def = entity.GetDefinition() as ksMirrorCopyDefinition + ?? throw new InvalidOperationException("GetDefinition() зеркала вернул не ksMirrorCopyDefinition."); + def.SetPlane(planeEntity); + var arr = def.GetOperationArray() as ksEntityCollection + ?? throw new InvalidOperationException("GetOperationArray() зеркала вернул не ksEntityCollection."); + foreach (var f in features) arr.Add(f); + + if (!entity.Create()) + throw new InvalidOperationException( + "Create() зеркальной копии вернул FALSE (копия пересекает исходную геометрию?)."); + + var id = _nextId++; + _features[id] = entity; + return id; + }, ct); + + /// Зеркально отразить всё тело относительно координатной плоскости plane (получить + /// симметричную деталь: оригинал + зеркало). Возвращает id операции. + public Task MirrorBodyAsync(BasePlane plane, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + var part = GetTopPart(); + var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity + ?? throw new InvalidOperationException("Не удалось получить координатную плоскость."); + + var entity = part.NewEntity((short)Obj3dType.o3d_mirrorAllOperation) as ksEntity + ?? throw new InvalidOperationException("NewEntity(o3d_mirrorAllOperation) вернул null."); + var def = entity.GetDefinition() as ksMirrorCopyAllDefinition + ?? throw new InvalidOperationException("GetDefinition() зеркала тела вернул не ksMirrorCopyAllDefinition."); + def.SetPlane(planeEntity); + // Отражаем все тела детали. + if (def.ChooseBodies() is ksChooseBodies chooser) + chooser.ChooseBodiesType = (int)ksChooseBodiesType.ksAllBodies; + + if (!entity.Create()) + throw new InvalidOperationException( + "Create() зеркального отражения тела вернул FALSE (тело пересекает плоскость симметрии?)."); + + var id = _nextId++; + _features[id] = entity; + return id; + }, ct); + /// Перестроить документ. public Task RebuildAsync(CancellationToken ct = default) => _dispatcher.InvokeAsync(() => diff --git a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs index 05662df..654b0a2 100644 --- a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs +++ b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs @@ -163,6 +163,27 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler) return $"Круговой массив создан ({count} экз. вокруг {axis}, шаг {step}°), id={id}."; } + [McpServerTool(Name = "mirror_operation")] + [Description("Зеркальная копия операций: отразить ранее созданные операции featureIds относительно координатной плоскости plane (XOY|XOZ|YOZ). Добавляет зеркальные копии (оригинал сохраняется). Возвращает id операции.")] + public async Task MirrorOperation( + [Description("Id операций-источников")] int[] featureIds, + [Description("Плоскость симметрии: XOY|XOZ|YOZ")] string plane) + { + await session.ConnectAsync(); + var id = await modeler.MirrorOperationAsync(featureIds, BasePlanes.Parse(plane)); + return $"Зеркальная копия операций создана (плоскость {plane}), id={id}."; + } + + [McpServerTool(Name = "mirror_body")] + [Description("Зеркально отразить всё тело детали относительно координатной плоскости plane (XOY|XOZ|YOZ) — получить симметричную деталь (оригинал + зеркало). Удобно для моделирования половины с последующим отражением. Возвращает id операции.")] + public async Task MirrorBody( + [Description("Плоскость симметрии: XOY|XOZ|YOZ")] string plane) + { + await session.ConnectAsync(); + var id = await modeler.MirrorBodyAsync(BasePlanes.Parse(plane)); + return $"Тело зеркально отражено (плоскость {plane}), id={id}."; + } + [McpServerTool(Name = "rebuild")] [Description("Перестроить активный документ.")] public async Task Rebuild() diff --git a/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs index 38d5c25..db7e682 100644 --- a/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs +++ b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs @@ -194,6 +194,67 @@ public sealed class FeatureOpsTests finally { await _docs.CloseAsync(save: false); } } + [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 Loft_two_squares_makes_solid() {