From 01f5f37c67a80b38070413795cd3844831e8d031 Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Tue, 26 May 2026 09:52:56 +0300 Subject: [PATCH] =?UTF-8?q?@=20feat(sketch):=20=D1=8D=D1=81=D0=BA=D0=B8?= =?UTF-8?q?=D0=B7=20=D0=BD=D0=B0=20=D0=B2=D1=8B=D0=B1=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=20+=20?= =?UTF-8?q?=D0=9C=D0=A6=D0=A5=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7=20API5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sketch_create_on_face: OpenSketchOnFaceAsync выбирает плоскую грань по точке (ksPart.EntityCollection(o3d_face).SelectByPoint) и создаёт на ней эскиз — ключевой шаг итеративного цикла «построил → выбрал грань → новый эскиз». Общее ядро создания эскиза вынесено в CreateSketchOn. QueryService переписан на API5 ksPart.CalcMassInertiaProperties(ST_MIX_MM|ST_MIX_KG): - API7 IMassInertiaParam7.Calculate() непригоден — флаг Actual «залипает» после первого расчёта, повторный запрос отдавал устаревший объём (баг кэша); - API5-маршрут считает по запросу и согласован с построением через ksPart; - единицы (мм³/мм²/кг/мм) задаются битовым вектором. Тест Sketch_on_face_adds_boss_on_top: запрос → штифт R5×H10 на верхней грани цилиндра → повторный запрос видит прирост ≈785 мм³ (проверяет и грань, и кэш МЦХ). Презентация: 20 инструментов, 15 тестов; OPEN_QUESTIONS обновлён. Co-Authored-By: Claude Opus 4.7 @ --- docs/OPEN_QUESTIONS.md | 23 +++++--- docs/presentation.html | 12 ++-- src/Kompas.Mcp.Core/Modeling/PartModeler.cs | 56 ++++++++++++++----- src/Kompas.Mcp.Core/Query/QueryService.cs | 52 ++++++++--------- src/Kompas.Mcp.Host/Tools/SketchTools.cs | 9 +++ .../Integration/ModelingTests.cs | 39 +++++++++++++ 6 files changed, 137 insertions(+), 54 deletions(-) diff --git a/docs/OPEN_QUESTIONS.md b/docs/OPEN_QUESTIONS.md index 2cd4000..ae836e7 100644 --- a/docs/OPEN_QUESTIONS.md +++ b/docs/OPEN_QUESTIONS.md @@ -69,15 +69,20 @@ stateful-сессии? Пока полагаемся на последовате передавать через `ImageContentBlock.FromBytes(bytes, mimeType)` — он сам кодирует. Прямое присваивание `Data = rawBytes` портит изображение на проводе. Исправлено в VisionTools. -### ✅ МЦХ (get_part_info): единицы измерения и поведение Calculate() -`IMassInertiaParam7` по умолчанию возвращает значения в **единицах отображения МЦХ** документа -(по умолчанию см³/см²/г, плотность г/см³) — то есть зависит от настроек, а не фиксированные СИ. -Поэтому `QueryService` принудительно выставляет `LengthUnits = ksLUnMM` и `MassUnits = ksMUnKG` -перед чтением → детерминированные **мм³, мм², кг, мм**. Проверено тестом (цилиндр R10×H20: -V≈6283 мм³, S≈1885 мм², m≈0.0494 кг, Zc=10 мм). -Тонкость: `Calculate()` возвращает **FALSE даже при успехе** (когда расчёт уже актуален после -`RebuildDocument`), поэтому опираемся на свойство `Actual`, а не на возвращаемое значение. -Материал по умолчанию — сталь (ρ≈7.856 г/см³); масса зависит от назначенного материала. +### ✅ МЦХ (get_part_info): API5 `CalcMassInertiaProperties`, а не API7 +Сначала пробовали API7 `IMassInertiaParam7` (от `TopPart`). Две проблемы: +1. **Единицы** — отдаёт в единицах отображения МЦХ документа (по умолчанию см³/см²/г), зависит от настроек. +2. **Залипающий кэш** — после первого расчёта свойство `Actual` остаётся `TRUE` и не сбрасывается + при изменении геометрии через API5; `Calculate()` возвращает `FALSE` и не пересчитывает. + Сценарий «запрос → достройка → запрос» отдавал **устаревшее** значение. Сброс через + `RebuildModel`, переключение `MassSettingMode` — не помогли. + +**Решение (✅ в коде):** считаем через API5 **`ksPart.CalcMassInertiaProperties(bitVector)`** → +`ksMassInertiaParam` (`v`/`m`/`F`/`xc`/`yc`/`zc`). Считает «по запросу» (без залипания), единицы +задаются битовым вектором: `ST_MIX_MM (0x1) | ST_MIX_KG (0x10)` → **мм³, мм², кг, мм**. +Согласовано с тем, что деталь строится через API5 `ksPart`. Проверено двумя тестами: +цилиндр R10×H20 (V≈6283 мм³, S≈1885 мм², m≈0.0494 кг, Zc=10 мм) и «запрос → штифт на грани → запрос» +(виден корректный прирост ≈785 мм³). Материал по умолчанию — сталь (ρ≈7.856 г/см³). ### ✅ Снимок 3D: рендер через файл, а не resultArrayBytes `ksDocument3D.SaveAsToRasterFormat(fileName, param)` при **непустом** `fileName` возвращает TRUE, diff --git a/docs/presentation.html b/docs/presentation.html index 0676fcd..646ebb7 100644 --- a/docs/presentation.html +++ b/docs/presentation.html @@ -125,7 +125,7 @@
- Статус: v1 работает — цикл эскиз→выдавливание→снимок через MCP (19 инструментов) + Статус: v1 работает — цикл эскиз→выдавливание→снимок через MCP (20 инструментов)

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

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

-
Общий прогрессv1 работает · 14 тестов зелёные · доки и ревью
-
+
Общий прогрессv1 + эскиз на грани · 15 тестов зелёные · доки и ревью
+
@@ -282,11 +282,11 @@

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

-

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

+

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

  • System: connect · status · set_visible
  • Documents: create · open · save · save_as · close · active
  • -
  • Sketch: create · add line/circle/rectangle · close
  • +
  • Sketch: create · create_on_face 🆕 · add line/circle/rectangle · close
  • Features: extrude_boss · extrude_cut · rebuild
  • Vision: model_snapshot 👁️
  • Query: get_part_info (объём/масса/площадь/центр масс)
  • @@ -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 2673625..5a1e019 100644 --- a/src/Kompas.Mcp.Core/Modeling/PartModeler.cs +++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs @@ -43,24 +43,52 @@ public sealed class PartModeler : IDisposable => _dispatcher.InvokeAsync(() => { var part = GetTopPart(); - var entity = part.NewEntity((short)Obj3dType.o3d_sketch) as ksEntity - ?? throw new InvalidOperationException("NewEntity(o3d_sketch) вернул null."); - var def = entity.GetDefinition() as ksSketchDefinition - ?? throw new InvalidOperationException("GetDefinition() эскиза вернул не ksSketchDefinition."); var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity ?? throw new InvalidOperationException("Не удалось получить базовую плоскость."); - - def.SetPlane(planeEntity); - entity.Create(); - - var editor = def.BeginEdit() as ksDocument2D - ?? throw new InvalidOperationException("BeginEdit() не вернул ksDocument2D."); - - var id = _nextId++; - _sketches[id] = new SketchEntry { Entity = entity, Definition = def, Editor = editor }; - return id; + return CreateSketchOn(part, planeEntity); }, ct); + /// + /// Создать эскиз на плоской грани детали, найденной по точке ,, + /// (мировые координаты модели, мм), и открыть его на редактирование. Возвращает id эскиза. + /// Это ключ к итеративному циклу «построил → выбрал грань → новый эскиз на ней». + /// + public Task OpenSketchOnFaceAsync(double x, double y, double z, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + var part = GetTopPart(); + var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection + ?? throw new InvalidOperationException("Не удалось получить коллекцию граней."); + // Оставляем в коллекции только грани, проходящие через указанную точку. + faces.SelectByPoint(x, y, z); + if (faces.GetCount() == 0) + throw new InvalidOperationException( + $"В точке ({x}, {y}, {z}) не найдено грани. Сделайте снимок и сверьте координаты."); + + var face = faces.GetByIndex(0) as ksEntity + ?? throw new InvalidOperationException("Грань из коллекции не приводится к ksEntity."); + return CreateSketchOn(part, face); + }, ct); + + /// Общее ядро: создать эскиз на заданной плоскости/грани и открыть на редактирование. + private int CreateSketchOn(ksPart part, ksEntity planeEntity) + { + var entity = part.NewEntity((short)Obj3dType.o3d_sketch) as ksEntity + ?? throw new InvalidOperationException("NewEntity(o3d_sketch) вернул null."); + var def = entity.GetDefinition() as ksSketchDefinition + ?? throw new InvalidOperationException("GetDefinition() эскиза вернул не ksSketchDefinition."); + + def.SetPlane(planeEntity); + entity.Create(); + + var editor = def.BeginEdit() as ksDocument2D + ?? throw new InvalidOperationException("BeginEdit() не вернул ksDocument2D."); + + var id = _nextId++; + _sketches[id] = new SketchEntry { Entity = entity, Definition = def, Editor = editor }; + return id; + } + /// Добавить отрезок в открытый эскиз. public Task AddLineAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default) => _dispatcher.InvokeAsync(() => diff --git a/src/Kompas.Mcp.Core/Query/QueryService.cs b/src/Kompas.Mcp.Core/Query/QueryService.cs index 6c9bce6..ea835be 100644 --- a/src/Kompas.Mcp.Core/Query/QueryService.cs +++ b/src/Kompas.Mcp.Core/Query/QueryService.cs @@ -1,8 +1,8 @@ using System.Runtime.InteropServices; using System.Runtime.Versioning; using Kompas.Mcp.Core.Threading; -using Kompas6Constants; -using KompasAPI7; +using Kompas6API5; +using Kompas6Constants3D; namespace Kompas.Mcp.Core.Query; @@ -10,6 +10,10 @@ namespace Kompas.Mcp.Core.Query; [SupportedOSPlatform("windows")] public sealed class QueryService { + // bitVector для CalcMassInertiaProperties: ST_MIX_MM (0x1, длина — мм) | ST_MIX_KG (0x10, масса — кг). + // Иначе КОМПАС вернёт значения в единицах по умолчанию (см/г). + private const int MixMmKg = 0x1 | 0x10; + private readonly KompasSession _session; private readonly KompasDispatcher _dispatcher; @@ -20,41 +24,39 @@ public sealed class QueryService } /// Рассчитать и вернуть МЦХ активной детали (объём, масса, площадь, центр масс). + /// + /// Используется API5 ksPart.CalcMassInertiaProperties — он считает МЦХ «по запросу» + /// и согласован с построением детали через API5. Путь API7 IMassInertiaParam7.Calculate() + /// здесь непригоден: его флаг Actual «залипает» после первого расчёта и не сбрасывается + /// при изменении геометрии через API5 (повторный запрос отдавал бы устаревшее значение). + /// public Task GetPartInfoAsync(CancellationToken ct = default) => _dispatcher.InvokeAsync(() => { - var doc3d = _session.Application.ActiveDocument as IKompasDocument3D - ?? throw new InvalidOperationException("Активный документ не является 3D-моделью."); - var part = doc3d.TopPart - ?? throw new InvalidOperationException("Не удалось получить вершинный компонент."); + var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D + ?? throw new InvalidOperationException("Нет активного 3D-документа."); + var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart + ?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart)."); + + ksMassInertiaParam? mass = null; try { - var mass = (IMassInertiaParam7)part; - - // Детерминированные единицы: длина — мм, масса — кг. Иначе КОМПАС вернёт - // значения в единицах отображения МЦХ (по умолчанию см/г), что зависит от настроек. - mass.LengthUnits = ksLengthUnitsEnum.ksLUnMM; - mass.MassUnits = ksMassUnitsEnum.ksMUnKG; - - // Calculate() возвращает FALSE и при успехе (когда расчёт уже актуален), - // поэтому ориентируемся на свойство Actual, а не на возвращаемое значение. - mass.Calculate(); - if (!mass.Actual) - throw new InvalidOperationException("МЦХ не рассчитаны (модель пуста или не перестроена?)."); + mass = part.CalcMassInertiaProperties(MixMmKg) as ksMassInertiaParam + ?? throw new InvalidOperationException("CalcMassInertiaProperties вернул null (модель пуста или не перестроена?)."); return new PartInfo { - Volume = mass.Volume, - Mass = mass.Mass, - Area = mass.Area, - CenterX = mass.Xc, - CenterY = mass.Yc, - CenterZ = mass.Zc, + Volume = mass.v, // объём, мм³ + Mass = mass.m, // масса, кг + Area = mass.F, // площадь, мм² + CenterX = mass.xc, // центр масс в глобальной СК, мм + CenterY = mass.yc, + CenterZ = mass.zc, }; } finally { - // part и его QI-приведение mass — один RCW; release один раз. + if (mass is not null && Marshal.IsComObject(mass)) Marshal.ReleaseComObject(mass); if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part); } }, ct); diff --git a/src/Kompas.Mcp.Host/Tools/SketchTools.cs b/src/Kompas.Mcp.Host/Tools/SketchTools.cs index 1f0b97b..482ba5e 100644 --- a/src/Kompas.Mcp.Host/Tools/SketchTools.cs +++ b/src/Kompas.Mcp.Host/Tools/SketchTools.cs @@ -18,6 +18,15 @@ public sealed class SketchTools(KompasSession session, PartModeler modeler) return $"Эскиз создан, id={id}. Добавьте геометрию (sketch_add_*) и завершите sketch_close."; } + [McpServerTool(Name = "sketch_create_on_face")] + [Description("Создать эскиз на плоской грани детали, выбранной по точке (мировые координаты модели x,y,z в мм), и открыть его на редактирование. Используйте для построения «на грани» поверх уже созданной геометрии: сделайте model_snapshot, прикиньте координаты точки на нужной грани, затем вызовите этот инструмент. Возвращает id эскиза.")] + public async Task CreateOnFace(double x, double y, double z) + { + await session.ConnectAsync(); + var id = await modeler.OpenSketchOnFaceAsync(x, y, z); + return $"Эскиз создан на грани в точке ({x}, {y}, {z}), id={id}. Координаты геометрии — в локальной плоскости эскиза."; + } + [McpServerTool(Name = "sketch_add_circle")] [Description("Добавить окружность в открытый эскиз. Координаты — в плоскости эскиза, мм.")] public async Task AddCircle(int sketchId, double centerX, double centerY, double radius) diff --git a/tests/Kompas.Mcp.Tests/Integration/ModelingTests.cs b/tests/Kompas.Mcp.Tests/Integration/ModelingTests.cs index 0c9eb8e..bafa373 100644 --- a/tests/Kompas.Mcp.Tests/Integration/ModelingTests.cs +++ b/tests/Kompas.Mcp.Tests/Integration/ModelingTests.cs @@ -1,5 +1,6 @@ using Kompas.Mcp.Core.Documents; using Kompas.Mcp.Core.Modeling; +using Kompas.Mcp.Core.Query; using Kompas.Mcp.Core.Vision; namespace Kompas.Mcp.Tests.Integration; @@ -12,12 +13,14 @@ public sealed class ModelingTests private readonly DocumentService _docs; private readonly PartModeler _modeler; private readonly SnapshotService _snap; + private readonly QueryService _query; public ModelingTests(KompasFixture fx) { _docs = new DocumentService(fx.Session, fx.Dispatcher); _modeler = new PartModeler(fx.Session, fx.Dispatcher); _snap = new SnapshotService(fx.Session, fx.Dispatcher); + _query = new QueryService(fx.Session, fx.Dispatcher); } [Fact] @@ -53,6 +56,42 @@ public sealed class ModelingTests } } + [Fact] + public async Task Sketch_on_face_adds_boss_on_top() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Цилиндр R10×H20 на XOY → верхняя грань на Z=20. + var s1 = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddCircleAsync(s1, 0, 0, 10); + await _modeler.CloseSketchAsync(s1); + await _modeler.ExtrudeAsync(s1, depth: 20); + + // Запрос ДО достройки — проверяем, что повторный запрос увидит изменения + // (МЦХ не должна «залипнуть» на устаревшем значении). + var before = await _query.GetPartInfoAsync(); + + // Эскиз на верхней грани (точка (0,0,20)) → штифт R5×H10 наружу. + var s2 = await _modeler.OpenSketchOnFaceAsync(0, 0, 20); + await _modeler.AddCircleAsync(s2, 0, 0, 5); + await _modeler.CloseSketchAsync(s2); + var boss = await _modeler.ExtrudeAsync(s2, depth: 10); + Assert.True(boss > 0); + await _modeler.RebuildAsync(); + + var after = await _query.GetPartInfoAsync(); + + // Прирост объёма ≈ объём штифта π·5²·10 ≈ 785.4 мм³. + var deltaExpected = Math.PI * 25 * 10; + Assert.InRange(after.Volume - before.Volume, deltaExpected * 0.9, deltaExpected * 1.1); + } + finally + { + await _docs.CloseAsync(save: false); + } + } + [Fact] public async Task Reset_invalidates_sketch_ids() {