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()
{