v1 · готово ✓
Цикл моделирования
Документы, эскизы, выдавливание/вырез, снимок модели — работает
-
v2 · в работе
Операции и выборка
Эскиз на грани ✓ · вращение ✓ · далее: скругление/фаска, массивы, список тел
+
v2 · в работе
Операции и выборка
Эскиз на грани ✓ · вращение ✓ · скругление/фаска ✓ · далее: массивы, список тел
v3
Параметрика
Переменные IVariable7, свойства IPropertyMng, формулы
v4
2D-черчение
Виды, размеры, тексты, штриховки; спецификации
v5
Сборки и экспорт
Компоненты, сопряжения, конвертеры (STEP), HTTP/SSE-транспорт
diff --git a/src/Kompas.Mcp.Core/Modeling/PartModeler.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs
index f208cba..3e51771 100644
--- a/src/Kompas.Mcp.Core/Modeling/PartModeler.cs
+++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs
@@ -246,6 +246,64 @@ public sealed class PartModeler : IDisposable
return id;
}, ct);
+ ///
+ /// Скруглить ребро детали, найденное по точке (мировые координаты, мм), радиусом .
+ /// Возвращает id операции.
+ ///
+ public Task
FilletEdgeAsync(double x, double y, double z, double radius, CancellationToken ct = default)
+ => _dispatcher.InvokeAsync(() =>
+ {
+ if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус скругления должен быть > 0.");
+ var part = GetTopPart();
+ var edge = SelectEdgeByPoint(part, x, y, z);
+
+ var entity = part.NewEntity((short)Obj3dType.o3d_fillet) as ksEntity
+ ?? throw new InvalidOperationException("NewEntity(o3d_fillet) вернул null.");
+ var def = entity.GetDefinition() as ksFilletDefinition
+ ?? throw new InvalidOperationException("GetDefinition() скругления вернул не ksFilletDefinition.");
+ def.radius = radius;
+ def.tangent = false;
+ var arr = def.array() as ksEntityCollection
+ ?? throw new InvalidOperationException("array() скругления вернул не ksEntityCollection.");
+ arr.Add(edge);
+
+ if (!entity.Create())
+ throw new InvalidOperationException("Create() скругления вернул FALSE (радиус слишком велик для ребра?).");
+
+ var id = _nextId++;
+ _features[id] = entity;
+ return id;
+ }, ct);
+
+ ///
+ /// Снять фаску с ребра детали, найденного по точке (мировые координаты, мм), катетом .
+ /// Возвращает id операции.
+ ///
+ public Task ChamferEdgeAsync(double x, double y, double z, double distance, CancellationToken ct = default)
+ => _dispatcher.InvokeAsync(() =>
+ {
+ if (distance <= 0) throw new ArgumentOutOfRangeException(nameof(distance), "Катет фаски должен быть > 0.");
+ var part = GetTopPart();
+ var edge = SelectEdgeByPoint(part, x, y, z);
+
+ var entity = part.NewEntity((short)Obj3dType.o3d_chamfer) as ksEntity
+ ?? throw new InvalidOperationException("NewEntity(o3d_chamfer) вернул null.");
+ var def = entity.GetDefinition() as ksChamferDefinition
+ ?? throw new InvalidOperationException("GetDefinition() фаски вернул не ksChamferDefinition.");
+ def.tangent = false;
+ def.SetChamferParam(true, distance, distance);
+ var arr = def.array() as ksEntityCollection
+ ?? throw new InvalidOperationException("array() фаски вернул не ksEntityCollection.");
+ arr.Add(edge);
+
+ 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(() =>
@@ -271,6 +329,19 @@ public sealed class PartModeler : IDisposable
=> ActiveDoc3D().GetPart((short)Part_Type.pTop_Part) as ksPart
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
+ /// Выбрать ребро детали по точке (мировые координаты). Берёт первое подходящее.
+ private static ksEntity SelectEdgeByPoint(ksPart part, double x, double y, double z)
+ {
+ var edges = part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection
+ ?? throw new InvalidOperationException("Не удалось получить коллекцию рёбер.");
+ edges.SelectByPoint(x, y, z);
+ if (edges.GetCount() == 0)
+ throw new InvalidOperationException(
+ $"В точке ({x}, {y}, {z}) не найдено ребра. Сделайте снимок и сверьте координаты точки на ребре.");
+ return edges.GetByIndex(0) as ksEntity
+ ?? throw new InvalidOperationException("Ребро из коллекции не приводится к ksEntity.");
+ }
+
private SketchEntry RequireSketch(int id)
=> _sketches.TryGetValue(id, out var s) ? s
: throw new KeyNotFoundException($"Эскиз с id={id} не найден.");
diff --git a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs
index a1f94eb..5f572e4 100644
--- a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs
+++ b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs
@@ -55,6 +55,24 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler)
return $"Вырез вращением (cut) создан, id={id}.";
}
+ [McpServerTool(Name = "fillet_edge")]
+ [Description("Скруглить ребро детали, выбранное по точке (мировые координаты x,y,z в мм, точка должна лежать на ребре), радиусом radius (мм). Сделайте model_snapshot, прикиньте точку на ребре. Возвращает id операции.")]
+ public async Task FilletEdge(double x, double y, double z, double radius)
+ {
+ await session.ConnectAsync();
+ var id = await modeler.FilletEdgeAsync(x, y, z, radius);
+ return $"Скругление R{radius} создано на ребре в точке ({x}, {y}, {z}), id={id}.";
+ }
+
+ [McpServerTool(Name = "chamfer_edge")]
+ [Description("Снять фаску с ребра детали, выбранного по точке (мировые координаты x,y,z в мм, точка должна лежать на ребре), катетом distance (мм). Сделайте model_snapshot, прикиньте точку на ребре. Возвращает id операции.")]
+ public async Task ChamferEdge(double x, double y, double z, double distance)
+ {
+ await session.ConnectAsync();
+ var id = await modeler.ChamferEdgeAsync(x, y, z, distance);
+ return $"Фаска {distance}×{distance} создана на ребре в точке ({x}, {y}, {z}), id={id}.";
+ }
+
[McpServerTool(Name = "rebuild")]
[Description("Перестроить активный документ.")]
public async Task Rebuild()
diff --git a/tests/Kompas.Mcp.Tests/Integration/ModelingTests.cs b/tests/Kompas.Mcp.Tests/Integration/ModelingTests.cs
index 15c611c..7698fee 100644
--- a/tests/Kompas.Mcp.Tests/Integration/ModelingTests.cs
+++ b/tests/Kompas.Mcp.Tests/Integration/ModelingTests.cs
@@ -120,6 +120,41 @@ public sealed class ModelingTests
}
}
+ [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()
{