From 88dad09ea72acd0a811bcac41b18e7aeaee67309 Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Tue, 26 May 2026 10:01:26 +0300 Subject: [PATCH] =?UTF-8?q?@=20feat(features):=20=D1=81=D0=BA=D1=80=D1=83?= =?UTF-8?q?=D0=B3=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=20=D1=84=D0=B0?= =?UTF-8?q?=D1=81=D0=BA=D0=B0=20=D1=80=D0=B5=D0=B1=D1=80=D0=B0=20(fillet?= =?UTF-8?q?=5Fedge=20/=20chamfer=5Fedge)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PartModeler.FilletEdgeAsync / ChamferEdgeAsync через API5 o3d_fillet/o3d_chamfer: ребро выбирается по точке (общий хелпер SelectEdgeByPoint поверх EntityCollection(o3d_edge).SelectByPoint), добавляется в def.array(), радиус/катет задаются (ksFilletDefinition.radius, ksChamferDefinition.SetChamferParam). Новые MCP-инструменты: fillet_edge, chamfer_edge. Тест Fillet_and_chamfer_edges: цилиндр → скругление верхнего ребра R3 + фаска нижнего 2×2, проверка успеха, уменьшения объёма и снимка (визуально подтверждено). Презентация: 25 инструментов, 17 тестов. Co-Authored-By: Claude Opus 4.7 @ --- docs/presentation.html | 12 ++-- src/Kompas.Mcp.Core/Modeling/PartModeler.cs | 71 +++++++++++++++++++ src/Kompas.Mcp.Host/Tools/FeatureTools.cs | 18 +++++ .../Integration/ModelingTests.cs | 35 +++++++++ 4 files changed, 130 insertions(+), 6 deletions(-) diff --git a/docs/presentation.html b/docs/presentation.html index 4e105ae..3568828 100644 --- a/docs/presentation.html +++ b/docs/presentation.html @@ -125,7 +125,7 @@
- Статус: v1 работает — цикл эскиз→выдавливание/вращение→снимок через MCP (23 инструмента) + Статус: v1 работает — цикл эскиз→операции→снимок через MCP (25 инструментов)

КОМПАС-3D MCP-сервер
управление CAD языком LLM

MCP-сервер, который превращает операции КОМПАС-3D — создание документов, эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом — @@ -246,8 +246,8 @@ впереди.

-
Общий прогрессv1 + эскиз на грани + вращение · 16 тестов зелёные · доки и ревью
-
+
Общий прогрессv2: грань · вращение · скругление/фаска · 17 тестов зелёные
+
@@ -282,12 +282,12 @@

Инструменты v1 ГОТОВО

-

23 инструмента, отдаются по MCP-протоколу

+

25 инструментов, отдаются по MCP-протоколу

  • System: connect · status · set_visible
  • Documents: create · open · save · save_as · close · active
  • Sketch: create · create_on_face · add line/circle/rectangle/axis · close
  • -
  • Features: extrude_boss/cut · revolve_boss/cut 🆕 · rebuild
  • +
  • Features: extrude · revolve · fillet_edge/chamfer_edge 🆕 · rebuild
  • Vision: model_snapshot 👁️
  • Query: get_part_info (объём/масса/площадь/центр масс)
  • Query: select_face · list_bodies (далее)
  • @@ -319,7 +319,7 @@ черчению и сборкам.

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