From 13ab0f65816e58e7e2e17c67ecfe9a643d38fe10 Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Wed, 27 May 2026 09:53:54 +0300 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=D0=BE=D1=82=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=B5=20(hole)=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20API7=20=E2=80=94=20IHoles3D/IHole3D=20+=20IHoleDisposa?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HoleService (Core/Modeling): простое цилиндрическое отверстие (ksHTBase), сквозное/глухое - размещение: BaseSurface=грань (FindObjectsByPoint) + AssociationVertex=Points3D.Add() (точка по мировым координатам ksPParamCoord+X/Y/Z); эскиз размещения не нужен - в API5 определения отверстия НЕТ (как draft) — реализовано на API7 (прецедент: move_face) - авто-направление в тело с повтором; DI-регистрация; инструмент hole в FeatureTools - интеграционные тесты: сквозное (Ø10 удаляет π·25·20) + глухое (Ø8×10); spike зелёный с первого раза - 103 теста зелёных; спек docs/superpowers/specs/2026-05-27-hole-design.md --- .../specs/2026-05-27-hole-design.md | 79 ++++++++++++++ src/Kompas.Mcp.Core/Modeling/HoleService.cs | 100 ++++++++++++++++++ src/Kompas.Mcp.Host/Program.cs | 1 + src/Kompas.Mcp.Host/Tools/FeatureTools.cs | 16 ++- .../Kompas.Mcp.Tests/Integration/HoleTests.cs | 74 +++++++++++++ 5 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 docs/superpowers/specs/2026-05-27-hole-design.md create mode 100644 src/Kompas.Mcp.Core/Modeling/HoleService.cs create mode 100644 tests/Kompas.Mcp.Tests/Integration/HoleTests.cs diff --git a/docs/superpowers/specs/2026-05-27-hole-design.md b/docs/superpowers/specs/2026-05-27-hole-design.md new file mode 100644 index 0000000..2740def --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-hole-design.md @@ -0,0 +1,79 @@ +# Дизайн: отверстие (hole) через API7 + +**Дата:** 2026-05-27 +**Статус:** реализовано и проверено (spike зелёный); на ревью (ревьюер — Codex) + +## Цель + +Добавить параметрическую операцию **«Отверстие»** (простое цилиндрическое, ksHTBase) — сквозное +или глухое, размещённое на грани по мировой точке. Приоритет 2 из плана. + +## Ключевой факт: отверстие — только API7 + +В **API5 определения отверстия НЕТ** (по рефлексии `Kompas6API5.dll` нет ни одного типа с `Hole`; +есть лишь `Obj3dType.o3d_holeOperation=52` без интерфейса — как было с draft). В **API7** — +богатая поддержка: `IModelContainer.Holes3D` (`IHoles3D`) → `Add()` → `IHole3D`, размещение через +`IHoleDisposal`. Прецедент API7 в слое моделирования уже есть — `move_face` (`FaceEditService`). +Поэтому отдельный сервис `HoleService` (API7), а не метод `PartModeler` (API5). + +## Подтверждённый workflow (spike зелёный) + +``` +IPart7 top = (IPart7)doc3d.TopPart; +IFace face = top.FindObjectsByPoint(x,y,z,true) → первый IFace; // как в move_face +IModelContainer c = (IModelContainer)top; // COM-QI + +// Точка размещения центра отверстия в МИРОВЫХ координатах: +IPoint3D pt = c.Points3D.Add(); +pt.ParameterType = ksPParamCoord; pt.X=x; pt.Y=y; pt.Z=z; pt.Update(); + +IHole3D hole = c.Holes3D.Add(); +hole.HoleType = ksHTBase; // простое цилиндрическое +hole.Diameter = diameter; // мм +hole.DepthType = throughAll ? ksDTReachThrough : ksDTValue; +if (!throughAll) hole.Depth = depth; // мм +hole.EndFaceType = ksEFFlat; hole.Axis=false; hole.ShowThread=false; + +IHoleDisposal d = (IHoleDisposal)hole; // QI +d.BaseSurface = (IModelObject)face; // обязательна грань +d.Perpendicular = true; +d.AssociationVertex = (IModelObject)pt; // центр = созданная точка +d.Direction = true; // в тело; при FALSE — пробуем обратное +hole.Update(); // повтор с Direction=false при отказе +doc3d.RebuildDocument(); +``` + +**Размещение** (главная неочевидность, решена): у `IHole3D` нет свойства позиции — оно в +`IHoleDisposal` (`BaseSurface` = грань + `AssociationVertex` = точка/вершина). Самый надёжный +способ задать центр по мировым координатам — создать `IPoint3D` (`ksPParamCoord` + X/Y/Z) и +передать его как `AssociationVertex`. Эскиз размещения НЕ требуется (в отличие от `extrude_cut`). + +Единицы: `Diameter`/`Depth`/`X`/`Y`/`Z` — мм. `EndFaceType=ksEFFlat` (плоский торец) для глухого. + +## MCP-инструмент (группа Feature) + +| Инструмент | Параметры | Поведение | +|---|---|---| +| `hole` | `x,y,z: double`, `diameter: double`, `depth=0`, `throughAll=false` | Просверлить цилиндрическое отверстие на грани, найденной по точке (x,y,z — центр). `throughAll` — сквозное (depth не нужен), иначе глухое на `depth`. Направление в тело — авто (как move_face). | + +**Решения (YAGNI):** на первом этапе только `ksHTBase` (простое цилиндрическое); зенковка/ +цековка/коническое (`ksHTCounterbore/Countersinking/Conic` + параметрические подынтерфейсы) и +резьба (`ShowThread`/`IThread`) — расширения позже. Глубина «до объекта» (`ksDTObject`) не +выставляется. Отверстие не регистрируется в реестре `_features` (он для API5-операций) — массивы/ +зеркало пакета C к нему неприменимы; контракт инструмента возвращает подтверждение без id. + +## Реализация + +- **`src/Kompas.Mcp.Core/Modeling/HoleService.cs`** (новый): `HoleAsync(x,y,z,diameter,depth,throughAll,ct)` + на STA-потоке; валидация `diameter>0`, `depth>0` (если не throughAll); `FindFaceAtPoint` + (копия паттерна из `FaceEditService`); авто-направление с повтором. +- **DI:** `AddSingleton()` в `Program.cs`. +- **Инструмент** `hole` в `FeatureTools.cs` (конструктор получает `HoleService`). +- Транзитные RCW не освобождаем точечно — консистентно (долг v2-2). + +## Тестирование (Integration, `HoleTests`) + +1. **Сквозное**: коробка 40×40×20 (центр 0,0; верх Z=20) → `hole(0,0,20, Ø10, throughAll)` → + удалён цилиндр π·25·20 ≈ 1570.8 мм³; `InRange(before-after, ±5%)`. ✓ +2. **Глухое**: та же коробка → `hole(0,0,20, Ø8, depth=10)` → удалён π·16·10 ≈ 502.7 мм³; + `InRange(before-after, ±10%)` (торец плоский, погрешность модели). ✓ diff --git a/src/Kompas.Mcp.Core/Modeling/HoleService.cs b/src/Kompas.Mcp.Core/Modeling/HoleService.cs new file mode 100644 index 0000000..0612496 --- /dev/null +++ b/src/Kompas.Mcp.Core/Modeling/HoleService.cs @@ -0,0 +1,100 @@ +using System.Runtime.Versioning; +using Kompas.Mcp.Core.Threading; +using Kompas6Constants3D; +using KompasAPI7; + +namespace Kompas.Mcp.Core.Modeling; + +/// +/// Операция «Отверстие» через API7 (IHoles3D/IHole3D). В API5 определения +/// отверстия нет, поэтому используется современный API7 (как и прямое редактирование грани). +/// +[SupportedOSPlatform("windows")] +public sealed class HoleService +{ + private readonly KompasSession _session; + private readonly KompasDispatcher _dispatcher; + + public HoleService(KompasSession session, KompasDispatcher dispatcher) + { + _session = session ?? throw new ArgumentNullException(nameof(session)); + _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); + } + + /// + /// Просверлить простое цилиндрическое отверстие (ksHTBase) на грани, найденной по мировой точке + /// (,,), с центром в этой точке. + /// diameter — диаметр (мм). throughAll — сквозное (depth игнорируется), иначе на глубину depth (мм). + /// + public Task HoleAsync(double x, double y, double z, double diameter, double depth, bool throughAll, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => HoleCore(x, y, z, diameter, depth, throughAll), ct); + + private void HoleCore(double x, double y, double z, double diameter, double depth, bool throughAll) + { + if (!(diameter > 0)) throw new ArgumentOutOfRangeException(nameof(diameter), "Диаметр должен быть > 0."); + if (!throughAll && !(depth > 0)) throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть > 0 (или throughAll)."); + + if (_session.Application.ActiveDocument is not IKompasDocument3D doc3d) + throw new InvalidOperationException("Активный документ не является 3D-документом."); + if (doc3d.TopPart is not IPart7 top) + throw new InvalidOperationException("Не удалось получить TopPart активного документа."); + + var face = FindFaceAtPoint(top, x, y, z) + ?? throw new InvalidOperationException( + $"В точке ({x}, {y}, {z}) не найдена грань. Сверьте координаты точки на грани (model_snapshot)."); + + var container = top as IModelContainer + ?? throw new InvalidOperationException("Деталь не приводится к IModelContainer."); + + // Точка размещения центра отверстия в мировых координатах. + var pt = container.Points3D.Add() + ?? throw new InvalidOperationException("Points3D.Add() вернул null."); + pt.ParameterType = ksPoint3DTypeEnum.ksPParamCoord; + pt.X = x; pt.Y = y; pt.Z = z; + if (!pt.Update()) + throw new InvalidOperationException("Update() точки размещения вернул FALSE."); + + var hole = container.Holes3D.Add() + ?? throw new InvalidOperationException("Holes3D.Add() вернул null."); + hole.HoleType = ksHoleTypeEnum.ksHTBase; + hole.Diameter = diameter; + hole.DepthType = throughAll ? ksDepthTypeEnum.ksDTReachThrough : ksDepthTypeEnum.ksDTValue; + if (!throughAll) hole.Depth = depth; + hole.EndFaceType = ksEndFaceTypeEnum.ksEFFlat; + hole.Axis = false; + hole.ShowThread = false; + + var disp = (IHoleDisposal)hole; + disp.BaseSurface = (IModelObject)face; + disp.Perpendicular = true; + disp.AssociationVertex = (IModelObject)pt; + disp.Direction = true; + + if (!hole.Update()) + { + disp.Direction = false; + if (!hole.Update()) + throw new InvalidOperationException("Update() отверстия вернул FALSE для обоих направлений."); + } + + doc3d.RebuildDocument(); + } + + private static IFace? FindFaceAtPoint(IPart7 part, double x, double y, double z) + { + var hit = part.FindObjectsByPoint(x, y, z, true); + switch (hit) + { + case IFace f: + return f; + case object[] arr: + foreach (var o in arr) if (o is IFace face) return face; + return null; + case System.Collections.IEnumerable en: + foreach (var o in en) if (o is IFace face) return face; + return null; + default: + return null; + } + } +} diff --git a/src/Kompas.Mcp.Host/Program.cs b/src/Kompas.Mcp.Host/Program.cs index c5fbbe3..83be864 100644 --- a/src/Kompas.Mcp.Host/Program.cs +++ b/src/Kompas.Mcp.Host/Program.cs @@ -22,6 +22,7 @@ builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs index 654b0a2..004c874 100644 --- a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs +++ b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs @@ -7,7 +7,7 @@ namespace Kompas.Mcp.Host.Tools; /// Инструменты формообразующих операций. [McpServerToolType] -public sealed class FeatureTools(KompasSession session, PartModeler modeler) +public sealed class FeatureTools(KompasSession session, PartModeler modeler, HoleService holes) { [McpServerTool(Name = "extrude_boss")] [Description("Выдавить эскиз, добавив материал (на глубину depth, мм). Возвращает id операции.")] @@ -184,6 +184,20 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler) return $"Тело зеркально отражено (плоскость {plane}), id={id}."; } + [McpServerTool(Name = "hole")] + [Description("Просверлить простое цилиндрическое отверстие (API7) на грани, выбранной по мировой точке (x,y,z в мм — точка должна лежать на грани и задаёт ЦЕНТР отверстия; сделайте model_snapshot/describe_face и прикиньте точку). diameter — диаметр (мм). throughAll=true — сквозное (depth не нужен); иначе глухое на глубину depth (мм). Направление в тело подбирается автоматически. Отличие от extrude_cut: параметрическое отверстие без эскиза.")] + public async Task Hole( + double x, double y, double z, + [Description("Диаметр отверстия, мм")] double diameter, + [Description("Глубина, мм (нужна, если throughAll=false)")] double depth = 0, + [Description("Сквозное отверстие через всё тело")] bool throughAll = false) + { + await session.ConnectAsync(); + await holes.HoleAsync(x, y, z, diameter, depth, throughAll); + var kind = throughAll ? "сквозное" : $"глухое на {depth} мм"; + return $"Отверстие Ø{diameter} ({kind}) создано на грани в точке ({x}, {y}, {z})."; + } + [McpServerTool(Name = "rebuild")] [Description("Перестроить активный документ.")] public async Task Rebuild() diff --git a/tests/Kompas.Mcp.Tests/Integration/HoleTests.cs b/tests/Kompas.Mcp.Tests/Integration/HoleTests.cs new file mode 100644 index 0000000..88d4a9e --- /dev/null +++ b/tests/Kompas.Mcp.Tests/Integration/HoleTests.cs @@ -0,0 +1,74 @@ +using Kompas.Mcp.Core.Documents; +using Kompas.Mcp.Core.Modeling; +using Kompas.Mcp.Core.Query; + +namespace Kompas.Mcp.Tests.Integration; + +/// Интеграция: операция «Отверстие» (API7 IHoles3D/IHole3D). +[Trait("Category", "Integration")] +[Collection(KompasCollection.Name)] +public sealed class HoleTests +{ + private readonly DocumentService _docs; + private readonly PartModeler _modeler; + private readonly HoleService _holes; + private readonly QueryService _query; + + public HoleTests(KompasFixture fx) + { + _docs = new DocumentService(fx.Session, fx.Dispatcher); + _modeler = new PartModeler(fx.Session, fx.Dispatcher); + _holes = new HoleService(fx.Session, fx.Dispatcher); + _query = new QueryService(fx.Session, fx.Dispatcher); + } + + [Fact] + public async Task Hole_through_top_face_removes_cylinder_volume() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Коробка 40×40×20 (центр в начале координат), верхняя грань на Z=20. + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddRectangleAsync(s, -20, -20, 20, 20); + await _modeler.CloseSketchAsync(s); + await _modeler.ExtrudeAsync(s, depth: 20); + var before = (await _query.GetPartInfoAsync()).Volume; // 32000 + + // Сквозное отверстие Ø10 по центру верхней грани (0,0,20). + await _holes.HoleAsync(0, 0, 20, diameter: 10, depth: 0, throughAll: true); + var after = (await _query.GetPartInfoAsync()).Volume; + + // Удалён цилиндр π·25·20 ≈ 1570.8 мм³. + var removed = Math.PI * 25 * 20; + Assert.True(after < before, "После отверстия объём должен уменьшиться."); + Assert.InRange(before - after, removed * 0.95, removed * 1.05); + } + finally { await _docs.CloseAsync(save: false); } + } + + [Fact] + public async Task Hole_blind_in_top_face_removes_partial_cylinder() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Коробка 40×40×20, верхняя грань на Z=20. + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddRectangleAsync(s, -20, -20, 20, 20); + await _modeler.CloseSketchAsync(s); + await _modeler.ExtrudeAsync(s, depth: 20); + var before = (await _query.GetPartInfoAsync()).Volume; // 32000 + + // Глухое отверстие Ø8 глубиной 10 по центру верхней грани. + await _holes.HoleAsync(0, 0, 20, diameter: 8, depth: 10, throughAll: false); + var after = (await _query.GetPartInfoAsync()).Volume; + + // Удалён цилиндр π·16·10 ≈ 502.7 мм³ (без учёта плоского торца — EndFaceType=Flat). + var removed = Math.PI * 16 * 10; + Assert.True(after < before, "После глухого отверстия объём должен уменьшиться."); + Assert.InRange(before - after, removed * 0.9, removed * 1.1); + } + finally { await _docs.CloseAsync(save: false); } + } +} From d3dd78ec0b8aaa1edd04a808d6b9be37fe885ec6 Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Wed, 27 May 2026 10:04:22 +0300 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= =?UTF-8?q?=20hole=20=D0=BF=D0=BE=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20Codex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - направление сверления — по факту убыли объёма (Update=TRUE даже при сверлении в воздух на сквозном отверстии); TryDirection + CurrentVolume (API5 МЦХ) — P2-баг, вскрыт тестом - валидация double.IsFinite для координат/диаметра/глубины (Infinity проходил мимо >0) — P2 - откат точки-сироты и объекта отверстия при неуспехе (IFeature7.Delete) + RebuildDocument — P2 - тест Hole_through_bottom_face_uses_direction_fallback (нижняя грань) — P3 - 104 теста зелёных --- .../specs/2026-05-27-hole-design.md | 20 +++-- src/Kompas.Mcp.Core/Modeling/HoleService.cs | 87 ++++++++++++++----- .../Kompas.Mcp.Tests/Integration/HoleTests.cs | 25 ++++++ 3 files changed, 105 insertions(+), 27 deletions(-) diff --git a/docs/superpowers/specs/2026-05-27-hole-design.md b/docs/superpowers/specs/2026-05-27-hole-design.md index 2740def..15ed4a1 100644 --- a/docs/superpowers/specs/2026-05-27-hole-design.md +++ b/docs/superpowers/specs/2026-05-27-hole-design.md @@ -38,11 +38,18 @@ IHoleDisposal d = (IHoleDisposal)hole; // QI d.BaseSurface = (IModelObject)face; // обязательна грань d.Perpendicular = true; d.AssociationVertex = (IModelObject)pt; // центр = созданная точка -d.Direction = true; // в тело; при FALSE — пробуем обратное -hole.Update(); // повтор с Direction=false при отказе -doc3d.RebuildDocument(); +d.Direction = true; // подбор по факту (см. ниже) +hole.Update(); doc3d.RebuildDocument(); // проверяем убыль объёма; иначе Direction=false ``` +**Направление сверления (важно):** нормаль грани ориентирована непредсказуемо (тот же класс +проблем, что у boss/cut в OPEN_QUESTIONS), а при **сквозном** отверстии `Update()` возвращает +`TRUE` даже когда сверлит «в воздух» (наружу тела → удалено 0 материала). Поэтому направление +подбираем **по факту удаления материала**: строим с `Direction=true`, перестраиваем, сравниваем +объём (API5 `CalcMassInertiaProperties`, согласован с API7-геометрией после `RebuildDocument`); +если объём не убыл — `Direction=false` и повтор; если оба не убавили — ошибка. Только успех +Update недостаточен. + **Размещение** (главная неочевидность, решена): у `IHole3D` нет свойства позиции — оно в `IHoleDisposal` (`BaseSurface` = грань + `AssociationVertex` = точка/вершина). Самый надёжный способ задать центр по мировым координатам — создать `IPoint3D` (`ksPParamCoord` + X/Y/Z) и @@ -65,8 +72,9 @@ doc3d.RebuildDocument(); ## Реализация - **`src/Kompas.Mcp.Core/Modeling/HoleService.cs`** (новый): `HoleAsync(x,y,z,diameter,depth,throughAll,ct)` - на STA-потоке; валидация `diameter>0`, `depth>0` (если не throughAll); `FindFaceAtPoint` - (копия паттерна из `FaceEditService`); авто-направление с повтором. + на STA-потоке; валидация `double.IsFinite` для координат/диаметра/глубины + `>0`; `FindFaceAtPoint` + (копия паттерна из `FaceEditService`); подбор направления по убыли объёма (`TryDirection`); + при неуспехе — откат точки и объекта отверстия (`IFeature7.Delete`), затем `RebuildDocument`. - **DI:** `AddSingleton()` в `Program.cs`. - **Инструмент** `hole` в `FeatureTools.cs` (конструктор получает `HoleService`). - Транзитные RCW не освобождаем точечно — консистентно (долг v2-2). @@ -77,3 +85,5 @@ doc3d.RebuildDocument(); удалён цилиндр π·25·20 ≈ 1570.8 мм³; `InRange(before-after, ±5%)`. ✓ 2. **Глухое**: та же коробка → `hole(0,0,20, Ø8, depth=10)` → удалён π·16·10 ≈ 502.7 мм³; `InRange(before-after, ±10%)` (торец плоский, погрешность модели). ✓ +3. **Нижняя грань (подбор направления)**: та же коробка → `hole(0,0,0, Ø10, throughAll)` со стороны + нижней грани → корректное направление внутрь (+Z) подбирается по убыли объёма; удалён π·25·20. ✓ diff --git a/src/Kompas.Mcp.Core/Modeling/HoleService.cs b/src/Kompas.Mcp.Core/Modeling/HoleService.cs index 0612496..c8fc9a9 100644 --- a/src/Kompas.Mcp.Core/Modeling/HoleService.cs +++ b/src/Kompas.Mcp.Core/Modeling/HoleService.cs @@ -1,5 +1,6 @@ using System.Runtime.Versioning; using Kompas.Mcp.Core.Threading; +using Kompas6API5; using Kompas6Constants3D; using KompasAPI7; @@ -31,8 +32,12 @@ public sealed class HoleService private void HoleCore(double x, double y, double z, double diameter, double depth, bool throughAll) { - if (!(diameter > 0)) throw new ArgumentOutOfRangeException(nameof(diameter), "Диаметр должен быть > 0."); - if (!throughAll && !(depth > 0)) throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть > 0 (или throughAll)."); + if (!double.IsFinite(x) || !double.IsFinite(y) || !double.IsFinite(z)) + throw new ArgumentException("Координаты точки отверстия должны быть конечными числами."); + if (!double.IsFinite(diameter) || diameter <= 0) + throw new ArgumentOutOfRangeException(nameof(diameter), "Диаметр должен быть конечным и > 0."); + if (!throughAll && (!double.IsFinite(depth) || depth <= 0)) + throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть конечной и > 0 (или throughAll)."); if (_session.Application.ActiveDocument is not IKompasDocument3D doc3d) throw new InvalidOperationException("Активный документ не является 3D-документом."); @@ -54,30 +59,68 @@ public sealed class HoleService if (!pt.Update()) throw new InvalidOperationException("Update() точки размещения вернул FALSE."); - var hole = container.Holes3D.Add() - ?? throw new InvalidOperationException("Holes3D.Add() вернул null."); - hole.HoleType = ksHoleTypeEnum.ksHTBase; - hole.Diameter = diameter; - hole.DepthType = throughAll ? ksDepthTypeEnum.ksDTReachThrough : ksDepthTypeEnum.ksDTValue; - if (!throughAll) hole.Depth = depth; - hole.EndFaceType = ksEndFaceTypeEnum.ksEFFlat; - hole.Axis = false; - hole.ShowThread = false; + var before = CurrentVolume(); - var disp = (IHoleDisposal)hole; - disp.BaseSurface = (IModelObject)face; - disp.Perpendicular = true; - disp.AssociationVertex = (IModelObject)pt; - disp.Direction = true; - - if (!hole.Update()) + // Если отверстие не построится — удаляем уже созданную точку (и сам объект отверстия), + // чтобы не засорять дерево «сиротами» при неуспехе. + IHole3D? hole = null; + try { - disp.Direction = false; - if (!hole.Update()) - throw new InvalidOperationException("Update() отверстия вернул FALSE для обоих направлений."); - } + hole = container.Holes3D.Add() + ?? throw new InvalidOperationException("Holes3D.Add() вернул null."); + hole.HoleType = ksHoleTypeEnum.ksHTBase; + hole.Diameter = diameter; + hole.DepthType = throughAll ? ksDepthTypeEnum.ksDTReachThrough : ksDepthTypeEnum.ksDTValue; + if (!throughAll) hole.Depth = depth; + hole.EndFaceType = ksEndFaceTypeEnum.ksEFFlat; + hole.Axis = false; + hole.ShowThread = false; + var disp = (IHoleDisposal)hole; + disp.BaseSurface = (IModelObject)face; + disp.Perpendicular = true; + disp.AssociationVertex = (IModelObject)pt; + + // Направление сверления подбираем ПО ФАКТУ удаления материала: нормаль грани может + // смотреть в любую сторону, а при сквозном отверстии Update() возвращает TRUE даже когда + // сверлит «в воздух» (наружу тела) — поэтому одного флага успеха Update недостаточно. + if (!TryDirection(hole, disp, doc3d, true, before) && + !TryDirection(hole, disp, doc3d, false, before)) + throw new InvalidOperationException( + "Отверстие не удалило материал ни в одном направлении (точка вне тела или диаметр/глубина несовместимы?)."); + } + catch + { + // Best-effort откат: сначала объект отверстия, затем точка размещения. + try { hole?.Owner?.Delete(); } catch { /* лучшее усилие */ } + try { pt.Owner?.Delete(); } catch { /* лучшее усилие */ } + doc3d.RebuildDocument(); + throw; + } + } + + /// Построить отверстие в заданном направлении и подтвердить, что материал убыл + /// (объём уменьшился). Возвращает true только при фактическом удалении материала. + private bool TryDirection(IHole3D hole, IHoleDisposal disp, IKompasDocument3D doc3d, bool direction, double before) + { + disp.Direction = direction; + if (!hole.Update()) return false; doc3d.RebuildDocument(); + return CurrentVolume() < before - 1e-6; + } + + /// Текущий объём детали (мм³) через API5 МЦХ — согласован с геометрией API7 после + /// RebuildDocument (как в QueryService). + private double CurrentVolume() + { + const int mixMmKg = 0x1 | 0x10; // ST_MIX_MM | ST_MIX_KG + var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D + ?? throw new InvalidOperationException("Нет активного 3D-документа (API5)."); + var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart + ?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart)."); + var mass = part.CalcMassInertiaProperties(mixMmKg) as ksMassInertiaParam + ?? throw new InvalidOperationException("CalcMassInertiaProperties вернул null."); + return mass.v; } private static IFace? FindFaceAtPoint(IPart7 part, double x, double y, double z) diff --git a/tests/Kompas.Mcp.Tests/Integration/HoleTests.cs b/tests/Kompas.Mcp.Tests/Integration/HoleTests.cs index 88d4a9e..46795f9 100644 --- a/tests/Kompas.Mcp.Tests/Integration/HoleTests.cs +++ b/tests/Kompas.Mcp.Tests/Integration/HoleTests.cs @@ -71,4 +71,29 @@ public sealed class HoleTests } finally { await _docs.CloseAsync(save: false); } } + + [Fact] + public async Task Hole_through_bottom_face_uses_direction_fallback() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Коробка 40×40×20, нижняя грань на Z=0 (нормаль смотрит наружу, −Z). + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddRectangleAsync(s, -20, -20, 20, 20); + await _modeler.CloseSketchAsync(s); + await _modeler.ExtrudeAsync(s, depth: 20); + var before = (await _query.GetPartInfoAsync()).Volume; // 32000 + + // Сверлим со стороны НИЖНЕЙ грани (0,0,0): корректное направление в тело — внутрь (+Z), + // т.е. противоположно нормали → проверяет авто-подбор направления (fallback). + await _holes.HoleAsync(0, 0, 0, diameter: 10, depth: 0, throughAll: true); + var after = (await _query.GetPartInfoAsync()).Volume; + + var removed = Math.PI * 25 * 20; + Assert.True(after < before, "Отверстие с нижней грани должно построиться (fallback направления)."); + Assert.InRange(before - after, removed * 0.95, removed * 1.05); + } + finally { await _docs.CloseAsync(save: false); } + } } From 0aab06169fb5f2fef8e834b462b1b254947dffe5 Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Wed, 27 May 2026 10:10:42 +0300 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20=D0=BE=D1=82=D0=B2=D0=B5=D1=80?= =?UTF-8?q?=D1=81=D1=82=D0=B8=D0=B5=20(hole,=20API7)=20=E2=80=94=20README/?= =?UTF-8?q?CLAUDE/ARCHITECTURE/OPEN=5FQUESTIONS/presentation,=2063=20?= =?UTF-8?q?=D0=B8=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D0=B0,=20104=20=D1=82=D0=B5=D1=81=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 5 +++-- README.md | 6 +++--- docs/ARCHITECTURE.md | 11 ++++++----- docs/OPEN_QUESTIONS.md | 9 +++++++++ docs/presentation.html | 18 +++++++++--------- 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6e920e0..5d36f3d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -25,7 +25,7 @@ The base is committed to the repo (canonical) — no regeneration step. It cover **v1 + v2 + STEP/assembly + direct B-rep edit + structural model inspection implemented and working** (full sketch→feature→inspect→STEP round-trip + move_face + describe_model validated end-to-end). Stack: **.NET 8 (`net8.0-windows`, x64), C#**, MCP via the official `ModelContextProtocol` SDK over **stdio**. -**62 MCP tools, 101 tests green (60 unit + 41 integration).** v2 added: sketch-on-face (by point and by index), revolve (boss/cut + sketch axis), fillet/chamfer (edge-by-point and by index), and Query tools `get_part_info` (МЦХ), `get_bounding_box`, `list_faces`, `list_edges`. After v2: `import_step`, `export_step` (`ConversionService`, `ConversionTools`), `list_components` (assembly traversal via `TopPart → IParts7`). `move_face` (`FaceEditService`, `EditTools`) — direct B-rep face editing. After move_face: **structural model inspection** — `describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure` (`ModelInspectionService`, `InspectionTools`). `describe_model` is the preferred first call over `model_snapshot` (no token cost for image context; gives feature tree, bodies, variables, topology summary, МЦХ). Package A «richer sketches»: 7 new sketch primitives — `sketch_add_arc`, `sketch_add_arc_3points`, `sketch_add_ellipse`, `sketch_add_polyline`, `sketch_add_polygon`, `sketch_add_spline`, `sketch_add_point`. `PartModeler` refactored into partial classes: `PartModeler.cs` (core), `PartModeler.Sketch.cs` (2D primitives), `PartModeler.Features.cs` (extrude/revolve/fillet/chamfer). Static `SketchGeometry` class in `Core/Modeling`. Package B «forming ops» complete: **`shell`** (оболочка, `o3d_shellOperation=43`) + **`rib`** (ребро жёсткости, `o3d_ribOperation=44`) + **`sweep`** (кинематическая операция, `o3d_baseEvolution=45`) + **`loft`** (по сечениям, `o3d_baseLoft=30`). Auxiliary geometry (start of package E): **`sketch_create_on_offset_plane`** (смещённая плоскость, `o3d_planeOffset=14`) — unlocked `loft`. Additional Edit tools: **`split_solid_by_plane`**, **`move_body`**, **`boolean_union`**. Package C «patterns & mirror» complete: **`linear_pattern`** (линейный массив, `o3d_meshCopy=35`) + **`circular_pattern`** (круговой массив, `o3d_circularCopy=36`) + **`mirror_operation`** (зеркальная копия операций, `o3d_mirrorOperation=48`) + **`mirror_body`** (зеркало тела, `o3d_mirrorAllOperation=49`). +**63 MCP tools, 104 tests green (60 unit + 44 integration).** v2 added: sketch-on-face (by point and by index), revolve (boss/cut + sketch axis), fillet/chamfer (edge-by-point and by index), and Query tools `get_part_info` (МЦХ), `get_bounding_box`, `list_faces`, `list_edges`. After v2: `import_step`, `export_step` (`ConversionService`, `ConversionTools`), `list_components` (assembly traversal via `TopPart → IParts7`). `move_face` (`FaceEditService`, `EditTools`) — direct B-rep face editing. After move_face: **structural model inspection** — `describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure` (`ModelInspectionService`, `InspectionTools`). `describe_model` is the preferred first call over `model_snapshot` (no token cost for image context; gives feature tree, bodies, variables, topology summary, МЦХ). Package A «richer sketches»: 7 new sketch primitives — `sketch_add_arc`, `sketch_add_arc_3points`, `sketch_add_ellipse`, `sketch_add_polyline`, `sketch_add_polygon`, `sketch_add_spline`, `sketch_add_point`. `PartModeler` refactored into partial classes: `PartModeler.cs` (core), `PartModeler.Sketch.cs` (2D primitives), `PartModeler.Features.cs` (extrude/revolve/fillet/chamfer). Static `SketchGeometry` class in `Core/Modeling`. Package B «forming ops» complete: **`shell`** (оболочка, `o3d_shellOperation=43`) + **`rib`** (ребро жёсткости, `o3d_ribOperation=44`) + **`sweep`** (кинематическая операция, `o3d_baseEvolution=45`) + **`loft`** (по сечениям, `o3d_baseLoft=30`). Auxiliary geometry (start of package E): **`sketch_create_on_offset_plane`** (смещённая плоскость, `o3d_planeOffset=14`) — unlocked `loft`. Additional Edit tools: **`split_solid_by_plane`**, **`move_body`**, **`boolean_union`**. Package C «patterns & mirror» complete: **`linear_pattern`** (линейный массив, `o3d_meshCopy=35`) + **`circular_pattern`** (круговой массив, `o3d_circularCopy=36`) + **`mirror_operation`** (зеркальная копия операций, `o3d_mirrorOperation=48`) + **`mirror_body`** (зеркало тела, `o3d_mirrorAllOperation=49`). **`hole`** (`HoleService`, `FeatureTools`) — цилиндрическое отверстие (сквозное или глухое) через **API7** (`IModelContainer`/`IHole3D`/`IHoleDisposal`); первая формообразующая операция на API7 (прецедент ранее только у `move_face`). See [`README.md`](README.md) and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md); design decisions/caveats in [`docs/OPEN_QUESTIONS.md`](docs/OPEN_QUESTIONS.md). @@ -52,7 +52,7 @@ Key implementation facts (don't relearn): - Logs go to **stderr** (stdout is the MCP channel). Integration tests reuse one КОМПАС via `KompasFixture`; artifacts land in gitignored `.scratch/`. - **Structural model inspection** (`ModelInspectionService`, `InspectionTools`): `describe_model` — one-call structural "passport" (bounding box + МЦХ + bodies + topology summary + feature tree + variables); preferred over snapshot. `list_features` — feature tree with params (depth/radius/legs). `list_bodies` — list solid/surface bodies with face count. `list_variables` — model variables (name/expression/value, external/info flags). `describe_face` — drill-down by index (type, area, normal, radius, edge count). `describe_edge` — drill-down by index (type, length, adjacent faces, vertices). `measure` — distance/angle between two objects (face|edge|vertex by index, unit `ST_MIX_MM`). - **Feature tree API** (don't relearn): read via API5 `ksPart.GetFeature()` → `(ksFeature).SubFeatureCollection(true,false)` → `ksFeatureCollection`. Cast `(ksFeature)part` does NOT work (RCW QI returns null) — must use `GetFeature()`. `ksFeature.type` returns only coarse `o3d_entity` and does NOT distinguish operations; precise type and params come from `ksFeature.GetObject()` → `ksEntity.GetDefinition()` and matching definition type (`ksBossExtrusionDefinition.GetSideParam`, `ksFilletDefinition.radius`, `ksChamferDefinition.GetChamferParam`, …). Node name ("Элемент выдавливания:1", "Скругление:1") is localized by КОМПАС itself. -- **STEP import/export, `list_components`, `move_face`, `split_solid_by_plane`, `move_body`, `boolean_union`, full inspection layer, richer sketch primitives (package A), `shell`, `rib`, `sweep`, `loft` (package B complete), `sketch_create_on_offset_plane` (package E start), `linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body` (package C complete) are done.** Not yet done: `draft` (no API5, requires API7), `hole`; parametrics (package D), 2D drawing, assembly building. Known caveat: boss/cut direction on a selected face depends on face-normal orientation (`forward` may need flipping — agent picks via snapshot or `describe_face` normal). See `docs/OPEN_QUESTIONS.md` → «Ревью v2». +- **STEP import/export, `list_components`, `move_face`, `split_solid_by_plane`, `move_body`, `boolean_union`, full inspection layer, richer sketch primitives (package A), `shell`, `rib`, `sweep`, `loft` (package B complete), `sketch_create_on_offset_plane` (package E start), `linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body` (package C complete), `hole` (package — API7) are done.** Not yet done: `draft` (no API5, requires API7); parametrics (package D), 2D drawing, assembly building. Known caveat: boss/cut direction on a selected face depends on face-normal orientation (`forward` may need flipping — agent picks via snapshot or `describe_face` normal). See `docs/OPEN_QUESTIONS.md` → «Ревью v2». - **"STEP import without history"** detected structurally: `bodyCount > 0 && no formative features && features.Count <= bodyCount+1` (only origin + body). STEP with edits (move_face: "Смещённая плоскость", "Разрезать", "Переместить грани", "Булева операция") — that already has history. - **Topology/measure API**: `ksFaceDefinition.EdgeCollection` / `GetCylinderParam` / `GetSurface().GetNormal` + `normalOrientation`; `ksEdgeDefinition.GetAdjacentFace(bool)` / `GetVertex(bool)` → `ksVertexDefinition.GetPoint`; bodies via `ksPart.BodyCollection()` → `ksBody` (`IsSolid` / `FaceCollection`); variables via `ksPart.VariableCollection()` → `ksVariable`; measurements via `ksPart.GetMeasurer()` → `ksMeasurer` (`SetObject1/2`, `unit=ST_MIX_MM`, `Calc`, `distance`/`MinDistance`/`angle` in degrees / `IsAngleValid`). - **STEP import COM pattern** (verified): `IApplication.get_Converter((object)(int)ksConverterFromSTEP=-3)` (pass format code, not DLL path) → `IConverter.ConverterParameters(cmd)` → set `IAdditionConvertParameters.Format=ksConverterFromSTEP` → `IKompasDocument3D1.ConvertFromAdditionFormat(path, prm)`. Export: `ConvertToAdditionFormat` with format codes for AP203/AP214/AP242. Param co-classes (AdditionConvertParameters etc.) are NOT CoCreatable — only via converter factories. Assembly traversal: `IKompasDocument3D.TopPart → IPart7.Parts (IParts7)`. Extract component: `prm.NeedCreateComponentsFiles=true` (writes .m3d files next to STEP) + `IPart7.OpenSourceDocument`. @@ -63,6 +63,7 @@ Key implementation facts (don't relearn): - **Offset plane (`sketch_create_on_offset_plane`, package E start)**: `OpenSketchOnOffsetPlaneAsync` in `PartModeler.Sketch.cs`. `ksPart.NewEntity(o3d_planeOffset=14)` → `ksPlaneOffsetDefinition` (`SetPlane(basePlane)` + `offset` mm + `direction`) → `Create()` → sketch on that plane. Used to place sketches at arbitrary heights — enables `loft` with parallel sections. `entity.Create()` result is checked (reliability fix applied to all `CreateSketchOn*` paths). - **Loft operation (`loft`, package B)**: `ksBaseLoftDefinition` via `ksPart.NewEntity(o3d_baseLoft=30)`. Params: `sketchIds[]` (≥2 closed profile sketches on different parallel planes). `Sketchs().Add(each sketch)` → `SetLoftParam(closed=false, false, true)` → `SetThinParam(false)` → `Create()`. Requires parallel section sketches at different heights — use `sketch_create_on_offset_plane` for the non-base sections. - **Patterns & mirror (package C)**: `linear_pattern` — `ksMeshCopyDefinition` via `ksPart.NewEntity(o3d_meshCopy=35)`; `SetAxis1(axis)` + `SetCopyParamAlongAxis(true,0,count,step,false)` + `count2=1` (disables 2nd direction → 1D linear); features added via `OperationArray()` (no Get). `circular_pattern` — `ksCircularCopyDefinition` via `NewEntity(o3d_circularCopy=36)`; `SetAxis(axis)`; properties set directly: `count1=1` (radial off), `count2=count` (ring), `step2=angle°` between neighbours, `factor2=false`, `inverce=reverse`; `GetOperationArray().Add(feature)`. `mirror_operation` — `ksMirrorCopyDefinition` via `NewEntity(o3d_mirrorOperation=48)`; `SetPlane(basePlane)` + `GetOperationArray().Add(feature)`. `mirror_body` — `ksMirrorCopyAllDefinition` via `NewEntity(o3d_mirrorAllOperation=49)`; `SetPlane(basePlane)`; `ChooseBodies()` does NOT cast to `ksChooseBodies` (returns null) and is not needed — `o3d_mirrorAllOperation` mirrors all bodies keeping the original (volume doubles). Source features retrieved by id from `_features` registry via new private `RequireFeature` helper (analogous to `RequireSketch`). New `src/Kompas.Mcp.Core/Modeling/CoordinateAxis.cs` — enum `CoordinateAxis {X,Y,Z}` + `CoordinateAxes.ToObj3dType/Parse` (→ `o3d_axisOX/OY/OZ = 71/72/73`), analogous to `BasePlane.cs`. `SketchGeometry.RequireMin(int value, int min, string paramName)` — validator for `count>=2`. Empirical: `count` includes the original instance (count=3 → 3 bodies); `count2=1` disables 2nd direction for linear; `step2` is angle° between neighbours for circular. +- **Hole operation (`hole`)**: implemented via **API7** (not API5 — `ksHoleDefinition` absent in interop, analogous to `draft`). New service `src/Kompas.Mcp.Core/Modeling/HoleService.cs` (NOT a `PartModeler` method). Tool signature: `hole(x, y, z, diameter, depth=0, throughAll=false)` — simple cylindrical hole (`ksHTBase`), blind or through-all, placed on the face found by world point. Returns confirmation text (no id — hole does NOT enter the `_features` API5 registry). Implementation: `(IModelContainer)part7` (COM-QI) → `Points3D.Add()` creates `IPoint3D` at world coords (centre point) → `Holes3D.Add()` → `IHole3D` (`HoleType=ksHTBase`, `Diameter`, `DepthType=ksDTReachThrough|ksDTValue`, `Depth`, `EndFaceType=ksEFFlat`) → `(IHoleDisposal)hole`: `BaseSurface=face` (via `FindObjectsByPoint`, same as `FaceEditService`), `Perpendicular=true`, `AssociationVertex=point` → `Update()` → `RebuildDocument()`. Placement via `IHoleDisposal` (no position property on `IHole3D`); no placement sketch needed (unlike `extrude_cut`). **Direction selection by volume delta**: `Update()` returns TRUE even when drilling outward (into air, 0 material removed). Strategy: try `Direction=true` → Rebuild → compare volume via API5 `CalcMassInertiaProperties`; if volume did not decrease → try `Direction=false`. Same class of problem as boss/cut face-normal ambiguity. On failure — roll back the orphan point and hole via `IFeature7.Delete()`. Input validation includes `double.IsFinite` check (Infinity passed `>0` guard). - **Richer sketch primitives (package A, ksDocument2D API5 wrappers)**: `ksArcBy3Points` (3-point arc), `ksArcByAngle` (centre/radius/start-end angles in degrees, counterClockwise flag), `ksEllipse` via `ksEllipseParam` — struct obtained via `KompasObject.GetParamStruct(ko_EllipseParam=22)`; **property names are `A`/`B` (uppercase) in the interop**; `ksRegularPolygon` via `ksRegularPolygonParam` (`ko_RegularPolygonParam=92`) — `describe = !inscribed`; NURBS spline via `ksNurbs(order=4)` + `ksNurbsPoint` loop + `ksEndObj`; `ksPoint`. Param structs are released after use. `PartModeler` split into partial classes: `PartModeler.cs` (registry/helpers/NewParam/reset), `PartModeler.Sketch.cs` (all 2D primitives), `PartModeler.Features.cs` (extrude/revolve/fillet/chamfer). Static `SketchGeometry` class (`Core/Modeling`) holds enum mappings (`ArcDirection`, `PolygonDescribe`) and validators (`RequirePositive`, `RequireVertexCount`, `RequirePoints`). Polyline/spline point lists use `record SketchPoint(X, Y)` with JSON names `x`/`y`. ## КОМПАС-3D API architecture (the critical context) diff --git a/README.md b/README.md index 3e74743..696608d 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,14 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe } ``` -## Инструменты (62 инструмента) +## Инструменты (63 инструмента) | Группа | Инструменты | |---|---| | System | `kompas_connect`, `kompas_status`, `kompas_set_visible` | | Documents | `document_create`, `document_open`, `document_save`, `document_save_as`, `document_close`, `document_active` | | Sketch | `sketch_create`, `sketch_create_on_face`, `sketch_create_on_face_index`, `sketch_create_on_offset_plane`, `sketch_add_line`, `sketch_add_circle`, `sketch_add_rectangle`, `sketch_add_axis`, `sketch_add_arc`, `sketch_add_arc_3points`, `sketch_add_ellipse`, `sketch_add_polyline`, `sketch_add_polygon`, `sketch_add_spline`, `sketch_add_point`, `sketch_close` | -| Features | `extrude_boss`, `extrude_cut`, `revolve_boss`, `revolve_cut`, `fillet_edge`, `chamfer_edge`, `fillet_edge_index`, `chamfer_edge_index`, `shell`, `rib`, `sweep`, `loft`, `linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`, `rebuild` | +| Features | `extrude_boss`, `extrude_cut`, `revolve_boss`, `revolve_cut`, `fillet_edge`, `chamfer_edge`, `fillet_edge_index`, `chamfer_edge_index`, `shell`, `rib`, `sweep`, `loft`, `linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`, `hole`, `rebuild` | | Edit | `move_face`, `split_solid_by_plane`, `move_body`, `boolean_union` | | Inspection | `describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure` | | Vision | `model_snapshot` (PNG-снимок; fallback — сначала `describe_model`) | @@ -70,7 +70,7 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe ``` src/Kompas.Mcp.Core/ COM-слой: STA-диспетчер, подключение, документы, эскизы/операции, конвертация, снимок, инспекция модели src/Kompas.Mcp.Host/ MCP-сервер (stdio) + определения инструментов -tests/Kompas.Mcp.Tests/ 101 тест: unit + integration (integration требуют КОМПАС) +tests/Kompas.Mcp.Tests/ 104 теста: unit + integration (integration требуют КОМПАС) libs/kompas-interop/ вендорские interop-сборки КОМПАС (из SDK Samples/Common) docs/ архитектура, план, презентация, MD-база знаний SDK (Kompas3D_SDK/) usecases/ полигон обкатки подходов (в .gitignore); приёмы поднимаются в навык kompas-3d diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 31a70b9..e7169f8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Архитектура MCP-сервера КОМПАС-3D (предлагаемый вариант) -> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection+package-A «богатые эскизы»+package-B «shell+rib+sweep+loft»+package-C «массивы и зеркало»+package-E «offset plane»). Актуализировано по коду. +> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection+package-A «богатые эскизы»+package-B «shell+rib+sweep+loft»+package-C «массивы и зеркало»+package-E «offset plane»+`hole`). Актуализировано по коду. > Сопутствующий контекст по COM API КОМПАС — в [`../CLAUDE.md`](../CLAUDE.md). > Имена интерфейсов и перечислений сверены со справкой SDK по MD-базе знаний `docs/Kompas3D_SDK/` (навык `kompas-sdk-research`). @@ -113,7 +113,7 @@ MCP client ──stdio──> Host (поток-пул, async) |---|---| | **Kompas.Mcp.Host** | Точка входа. Сборка MCP-сервера (stdio), DI (в т.ч. `ConversionService`), логи в stderr, старт и владение STA-потоком, graceful shutdown. Определения инструментов по категориям (System / Documents / Sketch / Features / Query / Conversion). **Тонкий слой**: валидация аргументов → постановка задачи на STA-поток → форматирование результата/ошибки в MCP-ответ. | | **Kompas.Mcp.Core** | COM-слой. Менеджер соединения, менеджер документов, построитель эскизов и операций, `ConversionService` (STEP импорт/экспорт), `QueryService` (МЦХ, грани, рёбра, компоненты), `ModelInspectionService` (дерево операций, тела, переменные, drill-down, измерения), снимок. Все паттерны API5/API7 живут здесь. | -| **Kompas.Mcp.Tests** | Юнит (без COM) + интеграционные (требуют КОМПАС). 101 тест (60 unit + 41 integration). | +| **Kompas.Mcp.Tests** | Юнит (без COM) + интеграционные (требуют КОМПАС). 104 теста (60 unit + 44 integration). | Принцип: **только `Core` знает про COM**. `Host`/Tools оперируют доменными DTO и вызывают `Core`; `Host` не содержит бизнес-логики. @@ -121,7 +121,7 @@ MCP client ──stdio──> Host (поток-пул, async) --- -## 5. Карта инструментов (62 инструмента) +## 5. Карта инструментов (63 инструмента) Сгруппированы вокруг центрального цикла «эскиз ↔ операция». Имена — `snake_case`. @@ -156,6 +156,7 @@ MCP client ──stdio──> Host (поток-пул, async) - `circular_pattern` — круговой массив вокруг координатной оси (`o3d_circularCopy=36`, `ksCircularCopyDefinition`); параметры: `featureIds[]`, `axis`, `count`, `step` (угол° между соседними), `reverse`, `geometric`. Свойства `count1=1`/`count2=count`/`step2=angle°`/`factor2=false`/`inverce=reverse` задаются напрямую; `GetOperationArray()`. - `mirror_operation` — зеркальная копия операций (`o3d_mirrorOperation=48`, `ksMirrorCopyDefinition`); параметры: `featureIds[]`, `plane` (XOY/XOZ/YOZ). `SetPlane` + `GetOperationArray()`. - `mirror_body` — зеркало всего тела (`o3d_mirrorAllOperation=49`, `ksMirrorCopyAllDefinition`); параметр: `plane`. `ChooseBodies()` не приводится к `ksChooseBodies` и не нужен — операция отражает все тела, сохраняя оригинал (объём удваивается). +- `hole` — цилиндрическое отверстие (сквозное или глухое) на грани, найденной по мировой точке (x,y,z). Параметры: `diameter` (мм), `depth` (мм, для глухого), `throughAll`. Реализовано через **API7** (`HoleService`, `src/Kompas.Mcp.Core/Modeling/HoleService.cs`) — первая формообразующая операция на API7 (в API5 интерфейс `ksHoleDefinition` отсутствует, аналогично `draft`). Паттерн: `(IModelContainer)part7` → `Points3D.Add()` (центр, `IPoint3D`) → `Holes3D.Add()` → `IHole3D` (`HoleType=ksHTBase`, `Diameter`, `DepthType`, `Depth`, `EndFaceType=ksEFFlat`) → `(IHoleDisposal)hole` (`BaseSurface=грань` через `FindObjectsByPoint`, `Perpendicular=true`, `AssociationVertex=point`) → `Update()` → `RebuildDocument()`. Направление сверления определяется **по факту убыли объёма**: `Direction=true` → Rebuild → сравнить объём (API5 `CalcMassInertiaProperties`); если не убыл → `Direction=false`. При неуспехе — откат через `IFeature7.Delete()`. Возвращает подтверждение без id (отверстие не попадает в реестр `_features` API5). - `rebuild` — перестроить деталь. **Edit** (прямое редактирование) @@ -321,10 +322,10 @@ dotnet build -c Release -r win-x64 # сборка ## 10. Дорожная карта -**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection+package-A+package-B+package-C+package-E start):** документы, эскизы (полный набор 2D-примитивов: линия, окружность, дуга, дуга по 3 точкам, прямоугольник, эллипс, ломаная, правильный многоугольник, сплайн NURBS, точка), **вспомогательная геометрия (`sketch_create_on_offset_plane`)**, выдавливание/вырез, вращение, скругление/фаска, **оболочка (`shell`)**, **ребро жёсткости (`rib`)**, **кинематическая операция (`sweep`)**, **операция по сечениям (`loft`)**, **линейный/круговой массив и зеркало (`linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`)**, снимок; `get_part_info`, `get_bounding_box`, `list_faces`, `list_edges`; `import_step`, `export_step`, `list_components`; **`move_face`**, **`split_solid_by_plane`**, **`move_body`**, **`boolean_union`** (прямое редактирование и булевы операции); **`describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure`** (структурный осмотр модели). +**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection+package-A+package-B+package-C+package-E start+hole):** документы, эскизы (полный набор 2D-примитивов: линия, окружность, дуга, дуга по 3 точкам, прямоугольник, эллипс, ломаная, правильный многоугольник, сплайн NURBS, точка), **вспомогательная геометрия (`sketch_create_on_offset_plane`)**, выдавливание/вырез, вращение, скругление/фаска, **оболочка (`shell`)**, **ребро жёсткости (`rib`)**, **кинематическая операция (`sweep`)**, **операция по сечениям (`loft`)**, **линейный/круговой массив и зеркало (`linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`)**, **отверстие (`hole`, API7)**, снимок; `get_part_info`, `get_bounding_box`, `list_faces`, `list_edges`; `import_step`, `export_step`, `list_components`; **`move_face`**, **`split_solid_by_plane`**, **`move_body`**, **`boolean_union`** (прямое редактирование и булевы операции); **`describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure`** (структурный осмотр модели). **Следующие приоритеты:** -1. `draft` (уклон, требует API7), `hole`. +1. `draft` (уклон, требует API7). 2. Пакет E «вспомогательная геометрия» (продолжение) — ось, точка, плоскость по трём точкам / по углу. 4. Рассечение/перемещение тела как MCP-инструменты (`SplitSolids`/`BodyRepositions`) — механика есть, продуктизация не закончена. 5. Свойства документа: `IPropertyMng` / `IPropertyKeeper`. diff --git a/docs/OPEN_QUESTIONS.md b/docs/OPEN_QUESTIONS.md index c9b6d2c..b906258 100644 --- a/docs/OPEN_QUESTIONS.md +++ b/docs/OPEN_QUESTIONS.md @@ -144,3 +144,12 @@ stateful-сессии? Пока полагаемся на последовате - `measure` — расстояние/угол между объектами (`ksMeasurer`). Ключевые паттерны: дерево — `ksPart.GetFeature()` → `SubFeatureCollection(true,false)`; тела — `BodyCollection()` → `ksBody`; переменные — `VariableCollection()` → `ksVariable`. Детект «импорт без истории»: `bodyCount>0 && нет формообразующих операций && features.Count<=bodyCount+1`. Итог: **62 инструмента, 101 тест.** + +### ✅ hole — реализован через API7 +**Решено.** Инструмент `hole` добавлен как первая формообразующая операция на API7 — в API5 интерфейс `ksHoleDefinition` отсутствует в interop (аналогично `draft`; `o3d_holeOperation=52` есть, но определения нет). Новый сервис `HoleService` (`src/Kompas.Mcp.Core/Modeling/HoleService.cs`) не является методом `PartModeler`. + +Паттерн размещения: `(IModelContainer)part7` → `Points3D.Add()` (создаёт `IPoint3D` как центр) → `Holes3D.Add()` → `IHole3D` → `(IHoleDisposal)hole` (`BaseSurface` = грань по `FindObjectsByPoint`, `Perpendicular=true`, `AssociationVertex=point`) → `Update()` → `RebuildDocument()`. Эскиз размещения не нужен. + +**Ключевая тонкость — направление по объёму:** `Update()` возвращает TRUE даже если сверлит «в воздух» снаружи тела (убыли материала нет). Решение: попытка `Direction=true` → Rebuild → сравниваем объём через API5 `CalcMassInertiaProperties`; если не убыл — пробуем `Direction=false`. Тот же класс проблем, что у boss/cut на грани (непредсказуемая ориентация нормали). При неуспехе обеих попыток — откат через `IFeature7.Delete()`. Добавлена валидация `double.IsFinite` (Infinity проходил мимо `>0`). + +Отверстие не попадает в реестр `_features` API5 → возвращает подтверждение без id. +3 интеграционных теста (`HoleTests.cs`). Итог: **63 инструмента, 104 теста (60 unit + 44 integration).** diff --git a/docs/presentation.html b/docs/presentation.html index 2e481f7..c86c2e5 100644 --- a/docs/presentation.html +++ b/docs/presentation.html @@ -128,7 +128,7 @@
- Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало (62 инструмента) + Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало+hole (63 инструмента)

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

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

-
Общий прогрессv3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell+rib+sweep+loft (пакет B) · массивы+зеркало (пакет C) · offset-plane (пакет E start) · навык kompas-3d · 62 инструмента · 101 тест
+
Общий прогрессv3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell+rib+sweep+loft (пакет B) · массивы+зеркало (пакет C) · offset-plane (пакет E start) · hole (API7) · навык kompas-3d · 63 инструмента · 104 теста
@@ -297,18 +297,18 @@

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

-

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

+

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

  • System: connect · status · set_visible
  • Documents: create · open · save · save_as · close · active
  • Sketch: create · on_face (точка/индекс) · on_offset_plane (смещённая плоскость) · add line/circle/arc/arc_3points/rectangle/axis/ellipse/polyline/polygon/spline/point · close
  • -
  • Features: extrude · revolve · fillet_edge/chamfer_edge (точка + индекс) · shell (оболочка) · rib (ребро жёсткости) · sweep (кинематическая) · loft (по сечениям) · linear_pattern · circular_pattern · mirror_operation · mirror_body · rebuild
  • +
  • Features: extrude · revolve · fillet_edge/chamfer_edge (точка + индекс) · shell (оболочка) · rib (ребро жёсткости) · sweep (кинематическая) · loft (по сечениям) · linear_pattern · circular_pattern · mirror_operation · mirror_body · hole (API7) · rebuild
  • Edit: move_face · split_solid_by_plane · move_body · boolean_union
  • Inspection: describe_model · list_features · list_bodies · list_variables · describe_face · describe_edge · measure
  • Vision: model_snapshot 👁️ (fallback)
  • Query: get_part_info · get_bounding_box · list_faces · list_edges · list_components
  • Conversion: import_step · export_step
  • -
  • draft (API7) · hole · пакет E (продолжение)
  • +
  • draft (API7) · пакет E (продолжение)
@@ -319,7 +319,7 @@
  • Unit-тесты — без COM: диспетчер, enum-маппинг, StepFormat, InspectionText
  • Интеграционные тесты с КОМПАС: подключение, документы, снимок, цикл, STEP round-trip, сборка, move_face, инспекция
  • -
  • Итого 101 тест зелёный (60 unit + 41 integration)
  • +
  • Итого 104 теста зелёных (60 unit + 44 integration)
  • Сборка kompas-mcp.exe, README с конфигом клиента
  • Навык kompas-3d (методика) + полигон usecases/
  • In-process тест MCP-клиента, релизный publish
  • @@ -341,9 +341,9 @@
    v2 · готово ✓

    Операции, выборка, STEP

    Эскиз на грани · вращение · скругление/фаска · list_faces/edges · import_step · export_step · list_components · навык kompas-3d

    v3 · готово ✓

    B-rep + структурный осмотр

    move_face (сдвиг грани на N мм) · describe_model · list_features · list_bodies · list_variables · describe_face · describe_edge · measure

    пакет A · готово ✓

    Богатые эскизы

    arc · arc_3points · ellipse · polyline · polygon · spline · point (+7 инструментов); PartModeler разбит на partial-классы; SketchGeometry

    -
    пакет B · готово ✓

    Формообразующие операции

    shell ✓ · rib ✓ · sweep ✓ · loft ✓ (по сечениям) · offset-plane ✓ (пакет E); draft · hole — впереди

    -
    пакет C · готово ✓

    Массивы и зеркало

    linear_pattern ✓ · circular_pattern ✓ · mirror_operation ✓ · mirror_body

    -
    далее · в работе

    draft · hole · пакет E

    draft (API7) · hole · пакет E продолжение (ось, угловая плоскость)

    +
    пакет B · готово ✓

    Формообразующие операции

    shell ✓ · rib ✓ · sweep ✓ · loft ✓ (по сечениям) · offset-plane ✓ (пакет E)

    +
    пакет C · готово ✓

    Массивы и зеркало

    linear_pattern ✓ · circular_pattern ✓ · mirror_operation ✓ · mirror_body ✓ · hole ✓ (API7)

    +
    далее · в работе

    draft · пакет E

    draft (API7, уклон) · пакет E продолжение (ось, угловая плоскость)