diff --git a/CLAUDE.md b/CLAUDE.md index 917fb2b..0defbe3 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**. -**50 MCP tools, 78 tests green (47 unit + 31 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, МЦХ). Latest (package A «richer sketches»): 7 new sketch primitives — `sketch_add_arc` (centre/radius/angles), `sketch_add_arc_3points` (3-point arc), `sketch_add_ellipse` (centre, semi-axes, angle), `sketch_add_polyline` (chain of segments, closed), `sketch_add_polygon` (regular polygon, inscribed/circumscribed), `sketch_add_spline` (cubic NURBS, order 4, closed), `sketch_add_point`. All angles in degrees. `PartModeler` refactored into partial classes: `PartModeler.cs` (core), `PartModeler.Sketch.cs` (2D primitives), `PartModeler.Features.cs` (extrude/revolve/fillet/chamfer). Clean mapping/validation extracted to `SketchGeometry` (static class in `Core/Modeling`); point lists for polyline/spline are `record SketchPoint(X,Y)` arrays with JSON names `x`/`y`. +**51 MCP tools, 79 tests green (47 unit + 32 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`. Latest (package B start): **`shell`** — convert body to shell: open selected faces (by index from `list_faces`), set wall thickness; params `faceIndices` (int[]), `thickness` (mm), `outward` (default: false = inward). Implemented via API5: `ksPart.NewEntity(o3d_shellOperation=43)` → `ksShellDefinition` → `FaceArray()` → `Create()`. See [`README.md`](README.md) and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md); design decisions/caveats in [`docs/OPEN_QUESTIONS.md`](docs/OPEN_QUESTIONS.md). @@ -45,18 +45,19 @@ dotnet run --project src/Kompas.Mcp.Host # start the MCP server (stdio) Key implementation facts (don't relearn): - **All COM calls run on one dedicated STA thread** (`KompasDispatcher.InvokeAsync`); КОМПАС is single-instance STA. Services never touch COM off that thread. - Connection goes through **API5** (`KOMPAS.Application.5` → `KompasObject`) then `ksGetApplication7()` → `IApplication`; the API5 root is needed for `ksPart` 3D-building and `ksDocument3D` snapshots. -- 3D is built via **API5 `ksPart`** (`NewEntity(o3d_sketch/o3d_bossExtrusion/o3d_bossRotated/o3d_fillet/o3d_chamfer/...)`, `ksBossExtrusionDefinition`/`ksCutExtrusionDefinition`/`ksBossRotatedDefinition`/`ksFilletDefinition`/`ksChamferDefinition`); cut-through holes use `dtBoth` + `etThroughAll`. API7 is used for app/documents. +- 3D is built via **API5 `ksPart`** (`NewEntity(o3d_sketch/o3d_bossExtrusion/o3d_bossRotated/o3d_fillet/o3d_chamfer/o3d_shellOperation=43/...)`, `ksBossExtrusionDefinition`/`ksCutExtrusionDefinition`/`ksBossRotatedDefinition`/`ksFilletDefinition`/`ksChamferDefinition`/`ksShellDefinition`); cut-through holes use `dtBoth` + `etThroughAll`. API7 is used for app/documents. - **Face/edge selection**: `ksPart.EntityCollection(o3d_face|o3d_edge)` → `SelectByPoint(x,y,z)` (world mm) filters to objects through the point, then `GetByIndex(0)`; or by stable index from `list_faces`/`list_edges`. `ksFaceDefinition.IsPlanar/IsCylinder/...` + `GetArea(ST_MIX_MM)` classify faces. `ksEdgeDefinition.IsLineSeg/IsCircle/IsArc/...` + `GetLength()` classify/measure edges. Sketch axis = line with system style **3** (осевая). Fillet/chamfer by edge index via `fillet_edge_index`/`chamfer_edge_index` (more reliable than by-point). - **МЦХ / mass props**: use **API5 `ksPart.CalcMassInertiaProperties(ST_MIX_MM|ST_MIX_KG)`** → `ksMassInertiaParam` (`v`/`m`/`F`/`xc`/`yc`/`zc`), computed on demand. **Do NOT** use API7 `IMassInertiaParam7.Calculate()` — its `Actual` flag sticks after the first calc and won't refresh on API5 geometry changes (stale volume). Bounding box: `ksPart.GetGabarit(full:false, ...)`. - **Snapshot**: `ksDocument3D.SaveAsToRasterFormat(file, ksRasterFormatParam)` renders to a temp file (the in-memory `resultArrayBytes` stays empty when a filename is given); read bytes back. Return to MCP via `ImageContentBlock.FromBytes(bytes, mime)` — **not** `Data = bytes` (Data holds base64 bytes). Use `model_snapshot` only for visually-spatial questions; **prefer `describe_model` first**. - 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`, full inspection layer, and richer sketch primitives (package A) are done.** Not yet done: body split/reposition as MCP tools (SplitSolids/BodyRepositions — driveable but not yet implemented); patterns/arrays (package C), 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`, full inspection layer, richer sketch primitives (package A), and `shell` (package B start) are done.** Not yet done: remaining package B forming ops (rib/draft/loft/sweep/hole), body split/reposition as MCP tools (SplitSolids/BodyRepositions — driveable but not yet implemented); patterns/arrays (package C), 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`. - **Direct B-rep face editing (`move_face`)** (verified on top_spacer, 39.45→41.45 mm height): API7 face object retrieved via `IPart7.FindObjectsByPoint(x,y,z,true)` → cast to `KompasAPI7.IFace`; containers acquired at runtime via COM-QI: `(ISurfaceContainer)part`, `(IModelContainer)part`; `FaceMover` (SetFaces + Offset + Direction + Update) moves the face. distance>0 = outward (add material), <0 = inward. Works on both parametric and imported B-rep geometry. Full split-reposition workflow (SplitSolids/BodyRepositions) is driveable but not yet exposed as tools. +- **Shell operation (`shell`, package B first step)**: `ksShellDefinition` via `ksPart.NewEntity(o3d_shellOperation=43)`; `thickness` (mm) + `thinType` (bool: `true`=inward/`false`=outward; mapping: `thinType = !outward`) + `FaceArray()` (open faces, ≥1, by index from `list_faces`). Helper `SelectFaceByIndex` in `PartModeler.cs` (analogous to `SelectEdgeByIndex`, with range check). Validation: `thickness>0`, dedup via `Distinct`, range check before mutation. Transient RCWs in `ShellAsync`/`SelectFaceByIndex` are intentionally not released (consistent with Fillet/Chamfer pattern). - **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 df57341..9121e71 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,14 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe } ``` -## Инструменты (50 инструментов) +## Инструменты (51 инструмент) | Группа | Инструменты | |---|---| | 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_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`, `rebuild` | +| Features | `extrude_boss`, `extrude_cut`, `revolve_boss`, `revolve_cut`, `fillet_edge`, `chamfer_edge`, `fillet_edge_index`, `chamfer_edge_index`, `shell`, `rebuild` | | Edit | `move_face` (прямое редактирование: сдвинуть грань на N мм вдоль нормали) | | 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/ 78 тестов: unit + integration (integration требуют КОМПАС) +tests/Kompas.Mcp.Tests/ 79 тестов: 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 b55aef7..a8131ec 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,6 +1,6 @@ # Архитектура MCP-сервера КОМПАС-3D (предлагаемый вариант) -> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection+package-A «богаче эскизы»). Актуализировано по коду. +> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection+package-A «богаче эскизы»+package-B start «shell»). Актуализировано по коду. > Сопутствующий контекст по 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) + интеграционные (требуют КОМПАС). 78 тестов (47 unit + 31 integration). | +| **Kompas.Mcp.Tests** | Юнит (без COM) + интеграционные (требуют КОМПАС). 79 тестов (47 unit + 32 integration). | Принцип: **только `Core` знает про COM**. `Host`/Tools оперируют доменными DTO и вызывают `Core`; `Host` не содержит бизнес-логики. @@ -121,7 +121,7 @@ MCP client ──stdio──> Host (поток-пул, async) --- -## 5. Карта инструментов (50 инструментов) +## 5. Карта инструментов (51 инструмент) Сгруппированы вокруг центрального цикла «эскиз ↔ операция». Имена — `snake_case`. @@ -146,6 +146,7 @@ MCP client ──stdio──> Host (поток-пул, async) **Features** (3D-операции) - `extrude_boss` / `extrude_cut` — sketch, depth, direction, endType. - `revolve_boss` / `revolve_cut` — угол. +- `shell` — превратить тело в оболочку: открыть грани по индексам (`faceIndices`, ≥1, из `list_faces`), задать толщину стенки (`thickness` мм), направление (`outward`, по умолчанию внутрь). Реализовано через API5 `ksPart.NewEntity(o3d_shellOperation=43)` → `ksShellDefinition` → `FaceArray()` → `Create()`. Маппинг: `thinType = !outward`. - `rebuild` — перестроить деталь. **Edit** (прямое редактирование) @@ -308,10 +309,10 @@ dotnet build -c Release -r win-x64 # сборка ## 10. Дорожная карта -**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection+package-A):** документы, эскизы (полный набор 2D-примитивов: линия, окружность, дуга, дуга по 3 точкам, прямоугольник, эллипс, ломаная, правильный многоугольник, сплайн NURBS, точка), выдавливание/вырез, вращение, скругление/фаска, снимок; `get_part_info`, `get_bounding_box`, `list_faces`, `list_edges`; `import_step`, `export_step`, `list_components`; **`move_face`** (прямое редактирование грани, работает на импортированной B-rep); **`describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure`** (структурный осмотр модели). +**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection+package-A+package-B start):** документы, эскизы (полный набор 2D-примитивов: линия, окружность, дуга, дуга по 3 точкам, прямоугольник, эллипс, ломаная, правильный многоугольник, сплайн NURBS, точка), выдавливание/вырез, вращение, скругление/фаска, **оболочка (`shell`)**, снимок; `get_part_info`, `get_bounding_box`, `list_faces`, `list_edges`; `import_step`, `export_step`, `list_components`; **`move_face`** (прямое редактирование грани, работает на импортированной B-rep); **`describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure`** (структурный осмотр модели). **Следующие приоритеты:** -1. Пакет B «формообразующие» — дополнительные 3D-операции (уклоны, рёбра, оболочка и др.). +1. Пакет B «формообразующие» (продолжение) — rib/draft/loft/sweep/hole. 2. Пакет C «массивы» — `ILinearPattern`, `ICircularPattern`. 3. Рассечение/перемещение тела как MCP-инструменты (`SplitSolids`/`BodyRepositions`) — механика есть, продуктизация не закончена. 4. Свойства документа: `IPropertyMng` / `IPropertyKeeper`. diff --git a/docs/OPEN_QUESTIONS.md b/docs/OPEN_QUESTIONS.md index 31e2d62..a2eb2fc 100644 --- a/docs/OPEN_QUESTIONS.md +++ b/docs/OPEN_QUESTIONS.md @@ -64,6 +64,9 @@ `EntityCollection(o3d_face/o3d_edge)` не освобождаются (унаследованный паттерн; `QueryService` их освобождает, `PartModeler` — только зарегистрированные сущности в `ResetCore`). Накапливается за длинную сессию. На проработку: единый `using`/release транзитов в построителе. + Тот же паттерн у новых членов (`SelectFaceByIndex`, `ShellAsync`: транзитные `ksShellDefinition`/ + `FaceArray()`/коллекции граней не освобождаются — сознательно, ради консистентности с + `Fillet`/`Chamfer`; войдёт в общий рефакторинг release транзитов). - v2-3 — **стабильность индексов граней** (`list_faces` → `sketch_create_on_face_index`): индекс валиден, пока геометрия не меняется между вызовами. После любой операции порядок/состав граней может измениться — переиспользовать старый индекс нельзя, нужно перезапросить `list_faces`. diff --git a/docs/presentation.html b/docs/presentation.html index fa8d6eb..6f3e716 100644 --- a/docs/presentation.html +++ b/docs/presentation.html @@ -128,7 +128,7 @@
- Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы (50 инструментов) + Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell (51 инструмент)

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

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

-
Общий прогрессv3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · навык kompas-3d · 50 инструментов · 78 тестов
+
Общий прогрессv3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell (пакет B) · навык kompas-3d · 51 инструмент · 79 тестов
@@ -297,18 +297,18 @@

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

-

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

+

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

  • System: connect · status · set_visible
  • Documents: create · open · save · save_as · close · active
  • Sketch: create · on_face (точка/индекс) · add line/circle/arc/arc_3points/rectangle/axis/ellipse/polyline/polygon/spline/point · close
  • -
  • Features: extrude · revolve · fillet_edge/chamfer_edge (точка + индекс) · rebuild
  • +
  • Features: extrude · revolve · fillet_edge/chamfer_edge (точка + индекс) · shell (оболочка) · rebuild
  • Edit: move_face (прямое редактирование B-rep)
  • 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
  • -
  • Пакет B (операции) · пакет C (массивы) · рассечение/перемещение тела (далее)
  • +
  • Пакет B (продолжение: rib/draft/loft/sweep/hole) · пакет C (массивы) · рассечение/перемещение тела
@@ -319,7 +319,7 @@
  • Unit-тесты — без COM: диспетчер, enum-маппинг, StepFormat, InspectionText
  • Интеграционные тесты с КОМПАС: подключение, документы, снимок, цикл, STEP round-trip, сборка, move_face, инспекция
  • -
  • Итого 78 тестов зелёных (47 unit + 31 integration)
  • +
  • Итого 79 тестов зелёных (47 unit + 32 integration)
  • Сборка kompas-mcp.exe, README с конфигом клиента
  • Навык kompas-3d (методика) + полигон usecases/
  • In-process тест MCP-клиента, релизный publish
  • @@ -341,7 +341,7 @@
    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/C · в работе

    Операции + массивы

    Пакет B (формообразующие) · пакет C (массивы/паттерны) · рассечение тела; свойства документа

    +
    пакет B · в работе

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

    shell ✓ (оболочка) · далее: rib · draft · loft · sweep · hole · пакет C (массивы) · рассечение тела

    далее

    2D + сборки + транспорт

    Виды, размеры, штриховки · построение сборок, сопряжения; HTTP/SSE-транспорт

diff --git a/docs/superpowers/plans/2026-05-26-shell.md b/docs/superpowers/plans/2026-05-26-shell.md new file mode 100644 index 0000000..a196b4f --- /dev/null +++ b/docs/superpowers/plans/2026-05-26-shell.md @@ -0,0 +1,170 @@ +# Shell (операция «Оболочка») Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps use checkbox (`- [ ]`). + +**Goal:** MCP-инструмент `shell` — превратить тело в оболочку заданной толщины, удалив выбранные грани. + +**Architecture:** `ShellAsync` в `PartModeler.Features.cs` (паттерн `NewEntity(o3d_shellOperation)→GetDefinition→FaceArray.Add→thickness/thinType→Create`), helper `SelectFaceByIndex` в ядре, инструмент `shell` в `FeatureTools.cs`. Грани — по индексам из `list_faces`. + +**Tech Stack:** .NET 8 x64, C#, COM API5 (`Kompas6API5.ksShellDefinition`, `Kompas6Constants3D.Obj3dType.o3d_shellOperation`=43). + +--- + +## Task 1: shell end-to-end + +**Files:** +- Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.cs` (helper) +- Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs` (метод) +- Modify: `src/Kompas.Mcp.Host/Tools/FeatureTools.cs` (инструмент) +- Test: `tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs` + +- [ ] **Step 1: Падающий интеграционный тест** `FeatureOpsTests.Shell_box_hollows_with_open_top` + +```csharp +using Kompas.Mcp.Core.Documents; +using Kompas.Mcp.Core.Modeling; +using Kompas.Mcp.Core.Query; + +namespace Kompas.Mcp.Tests.Integration; + +/// Интеграция: формообразующие операции пакета B. +[Trait("Category", "Integration")] +[Collection(KompasCollection.Name)] +public sealed class FeatureOpsTests +{ + private readonly DocumentService _docs; + private readonly PartModeler _modeler; + private readonly QueryService _query; + + public FeatureOpsTests(KompasFixture fx) + { + _docs = new DocumentService(fx.Session, fx.Dispatcher); + _modeler = new PartModeler(fx.Session, fx.Dispatcher); + _query = new QueryService(fx.Session, fx.Dispatcher); + } + + [Fact] + public async Task Shell_box_hollows_with_open_top() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Коробка 40×30×20. + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddRectangleAsync(s, 0, 0, 40, 30); + await _modeler.CloseSketchAsync(s); + await _modeler.ExtrudeAsync(s, depth: 20); + var before = (await _query.GetPartInfoAsync()).Volume; // 24000 + + // Удаляем одну из двух плоских граней-торцов (площадь 40×30=1200) → открытая оболочка. + var faces = await _query.ListFacesAsync(); + var cap = faces.First(f => f.Type == "plane" && Math.Abs(f.Area - 1200) < 1); + var id = await _modeler.ShellAsync(new[] { cap.Index }, thickness: 2, outward: false); + Assert.True(id > 0); + await _modeler.RebuildAsync(); + + // Оболочка 2 мм с открытым торцом: 24000 − 36×26×18 = 7152 мм³. + var after = (await _query.GetPartInfoAsync()).Volume; + Assert.True(after < before); + Assert.InRange(after, 7152 * 0.95, 7152 * 1.05); + } + finally { await _docs.CloseAsync(save: false); } + } +} +``` + +- [ ] **Step 2: Запустить — убедиться, что падает** + +Run: `dotnet test -c Release --filter "FullyQualifiedName~FeatureOpsTests.Shell_box_hollows_with_open_top"` +Expected: FAIL (метод `ShellAsync` не существует). + +- [ ] **Step 3: Helper `SelectFaceByIndex`** в `PartModeler.cs` (рядом с `SelectEdgeByIndex`) + +```csharp +/// Выбрать грань детали по индексу из list_faces (с проверкой диапазона). +private static ksEntity SelectFaceByIndex(ksPart part, int faceIndex) +{ + var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection + ?? throw new InvalidOperationException("Не удалось получить коллекцию граней."); + if (faceIndex < 0 || faceIndex >= faces.GetCount()) + throw new ArgumentOutOfRangeException(nameof(faceIndex), + $"Индекс грани вне диапазона [0; {faces.GetCount() - 1}]. Сверьтесь с list_faces."); + return faces.GetByIndex(faceIndex) as ksEntity + ?? throw new InvalidOperationException("Грань по индексу не приводится к ksEntity."); +} +``` + +- [ ] **Step 4: Метод `ShellAsync`** в `PartModeler.Features.cs` + +```csharp +/// Превратить тело в оболочку: удалить (открыть) грани с индексами faceIndices, +/// задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется), +/// true — наружу. Возвращает id операции. +public Task ShellAsync(IReadOnlyList faceIndices, double thickness, bool outward = false, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + if (thickness <= 0) throw new ArgumentOutOfRangeException(nameof(thickness), "Толщина должна быть > 0."); + ArgumentNullException.ThrowIfNull(faceIndices); + var indices = faceIndices.Distinct().ToList(); + if (indices.Count == 0) throw new ArgumentException("Нужна хотя бы одна удаляемая грань.", nameof(faceIndices)); + + var part = GetTopPart(); + // Проверяем диапазон ВСЕХ индексов до мутации модели. + var faces = indices.Select(i => SelectFaceByIndex(part, i)).ToList(); + + var entity = part.NewEntity((short)Obj3dType.o3d_shellOperation) as ksEntity + ?? throw new InvalidOperationException("NewEntity(o3d_shellOperation) вернул null."); + var def = entity.GetDefinition() as ksShellDefinition + ?? throw new InvalidOperationException("GetDefinition() оболочки вернул не ksShellDefinition."); + var arr = def.FaceArray() as ksEntityCollection + ?? throw new InvalidOperationException("FaceArray() оболочки вернул не ksEntityCollection."); + foreach (var f in faces) arr.Add(f); + def.thickness = thickness; + def.thinType = !outward; // true=внутрь, false=наружу + + if (!entity.Create()) + throw new InvalidOperationException( + "Create() оболочки вернул FALSE (толщина больше локального радиуса/стенки или несовместимая топология?)."); + + var id = _nextId++; + _features[id] = entity; + return id; + }, ct); +``` + +- [ ] **Step 5: Инструмент `shell`** в `FeatureTools.cs` (перед `Rebuild`) + +```csharp +[McpServerTool(Name = "shell")] +[Description("Превратить тело активной детали в оболочку (придать стенкам толщину): удалить (открыть) грани с индексами faceIndices из list_faces и задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется), true — наружу. Возвращает id операции.")] +public async Task Shell( + [Description("Индексы удаляемых (открываемых) граней из list_faces")] int[] faceIndices, + [Description("Толщина стенки, мм")] double thickness, + [Description("Толщина наружу (true) или внутрь (false)")] bool outward = false) +{ + await session.ConnectAsync(); + var id = await modeler.ShellAsync(faceIndices, thickness, outward); + return $"Оболочка толщиной {thickness} мм создана (удалено граней: {faceIndices.Length}), id={id}."; +} +``` + +- [ ] **Step 6: Запустить тест** + +Run: `dotnet test -c Release --filter "FullyQualifiedName~FeatureOpsTests.Shell_box_hollows_with_open_top"` +Expected: PASS (если фактический объём иной — скорректировать ожидание по замеру, сохранив узкий допуск). + +- [ ] **Step 7: Commit** + +```bash +git add src/Kompas.Mcp.Core/Modeling/PartModeler.cs src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs src/Kompas.Mcp.Host/Tools/FeatureTools.cs tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs +git commit -m "feat(feature): операция Оболочка (shell)" +``` + +--- + +## Task 2: документация и завершение + +- [ ] Полный прогон: `dotnet test -c Release --filter "Category=Unit"` и `--filter "Category=Integration"` — всё зелёное. +- [ ] Codex-ревью реализации; внести обоснованные правки. +- [ ] `docs-delegate`: +1 инструмент (→51), +1 тест; `shell` в описание формообразующих; начат пакет B. +- [ ] Merge в `main`. diff --git a/docs/superpowers/specs/2026-05-26-shell-design.md b/docs/superpowers/specs/2026-05-26-shell-design.md new file mode 100644 index 0000000..203bd5f --- /dev/null +++ b/docs/superpowers/specs/2026-05-26-shell-design.md @@ -0,0 +1,88 @@ +# Дизайн: пакет B (шаг 1) — операция «Оболочка» (shell) + +**Дата:** 2026-05-26 +**Статус:** на ревью (автономный режим; ревьюер — Codex) +**Контекст:** kompas3d-mcp — MCP-сервер для КОМПАС-3D через COM API (.NET 8, C#). + +## Цель + +Добавить формообразующую операцию **«Оболочка»** (придать стенкам толщину, удалив выбранные +грани) — первый шаг пакета B «формообразующие». Самая востребованная операция этого пакета; +опирается на уже готовый выбор грани по индексу из `list_faces`, без мульти-эскизной +оркестрации (нужной для loft/sweep). Один MCP-инструмент `shell`. + +## Не входит в объём (YAGNI) + +- Остальные операции пакета B (rib, draft, loft, sweep, hole) — отдельные шаги. +- Переменная толщина по граням (`ksShellDefinition` задаёт одну общую `thickness`). +- Закрытая оболочка без удаляемых граней — КОМПАС требует ≥1 удаляемую грань. + +## Сигнатуры COM (подтверждены: рефлексия interop + SDK-база + пример Step3d1.cs) + +``` +Obj3dType.o3d_shellOperation = 43 +ksShellDefinition: + double thickness // толщина стенки, мм + bool thinType // true = внутрь (габарит сохраняется), false = наружу + object FaceArray() // ksEntityCollection удаляемых (открываемых) граней; нужна >=1 +``` + +**Паттерн** (из `Samples/CSharp.zip → Step3d1.cs`): +``` +ksEntity ent = part.NewEntity(o3d_shellOperation) +ksShellDefinition def = ent.GetDefinition() +ksEntityCollection faces = def.FaceArray() +faces.Add(<грань>) // одна или несколько удаляемых граней +def.thickness = t; def.thinType = !outward +ent.Create() +``` +Порядок: сначала заполнить `FaceArray`, затем `thickness`/`thinType`, затем `Create()`. + +## Новый MCP-инструмент (группа Feature) + +| Инструмент | Параметры | Поведение | +|---|---|---| +| `shell` | `faceIndices: int[]`, `thickness: double`, `outward: bool = false` | Превратить тело в оболочку толщиной `thickness` мм, удалив (открыв) грани с указанными индексами из `list_faces`. `outward=false` — толщина внутрь (габарит сохраняется), `true` — наружу. | + +**Маппинг:** `thinType = !outward`. + +**Выбор граней:** по стабильному индексу из `list_faces` (как `sketch_create_on_face_index`, +`fillet_edge_index`). Индексы валидны, пока геометрия не менялась — берутся из свежего +`list_faces`. Несколько граней → несколько `FaceArray.Add`. + +## Реализация + +- **Метод модели** `ShellAsync(IReadOnlyList faceIndices, double thickness, bool outward, CancellationToken)` + в `PartModeler.Features.cs` (формообразующая). Возвращает id операции (как extrude/revolve). +- **Helper** `SelectFaceByIndex(ksPart, int)` в ядре `PartModeler.cs` — аналог существующего + `SelectEdgeByIndex` (диапазон-проверка + `EntityCollection(o3d_face).GetByIndex`). +- **Инструмент** `shell` в `FeatureTools.cs` (как `extrude_boss`/`revolve_boss`). +- **Валидация** (inline, как `ExtrudeAsync`/`RevolveAsync`): `thickness > 0`; `faceIndices` + не null и не пуст; **каждый индекс проверяется на диапазон до мутации модели** + (`SelectFaceByIndex` бросает `ArgumentOutOfRangeException`, как `SelectEdgeByIndex`); + **дубли индексов отбрасываются** (`Distinct`), чтобы не добавлять грань в `FaceArray` дважды; + `FaceArray()` проверяется на null (`as ksEntityCollection ?? throw`). Возврат + `Create()==FALSE` → `InvalidOperationException` с пояснением вероятных причин (толщина больше + локального радиуса/толщины стенки; несовместимая топология). +- **Известное ограничение** (документируем, не обрабатываем): на телах со скруглениями/фасками и + на вогнутых/криволинейных гранях оболочка может не построиться (`Create()==FALSE`) даже при + формально корректных входных данных — это ограничение ядра КОМПАС, причину API не сообщает. + +## Тестирование + +**Integration** (`Category=Integration`, `KompasFixture`, в `SketchPrimitivesTests` или новом +`FeatureOpsTests`): коробка 40×30×20 (`AddRectangle` → `Extrude`) → `list_faces` → найти +верхнюю грань (нормаль +Z или по площади) → `ShellAsync([верхняя], thickness=2, outward=false)` +→ объём существенно уменьшается (тело стало полым с открытым верхом) и остаётся > 0. +Точная проверка: оболочка 2 мм с открытым верхом = `24000 − 36×26×18 = 24000 − 16848 = 7152 мм³`. +Утверждение: `after < before` и `after ∈ (7152·0.95, 7152·1.05)` (≈6794…7510 мм³). Если КОМПАС +считает полость иначе — скорректировать ожидание по фактическому замеру, сохранив узкий допуск. + +Unit отдельный не вводим: маппинг `thinType=!outward` тривиален и инлайнится; валидация +параметров — в стиле существующих операций (`extrude`/`revolve` тоже покрыты интеграционно). + +## Влияние на документацию + +После реализации (через `docs-delegate`): счётчик инструментов +1 (→51), тестов +1; +добавить `shell` в описание формообразующих в `CLAUDE.md`/`README.md`/`docs/ARCHITECTURE.md`/ +`docs/presentation.html`; отметить начало пакета B. diff --git a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs index 8e03bcd..08359ac 100644 --- a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs +++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs @@ -196,6 +196,40 @@ public sealed partial class PartModeler return id; } + /// Превратить тело в оболочку: удалить (открыть) грани с индексами faceIndices, + /// задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется), + /// true — наружу. Возвращает id операции. + public Task ShellAsync(IReadOnlyList faceIndices, double thickness, bool outward = false, CancellationToken ct = default) + => _dispatcher.InvokeAsync(() => + { + if (thickness <= 0) throw new ArgumentOutOfRangeException(nameof(thickness), "Толщина должна быть > 0."); + ArgumentNullException.ThrowIfNull(faceIndices); + var indices = faceIndices.Distinct().ToList(); + if (indices.Count == 0) throw new ArgumentException("Нужна хотя бы одна удаляемая грань.", nameof(faceIndices)); + + var part = GetTopPart(); + // Проверяем диапазон ВСЕХ индексов до мутации модели. + var faces = indices.Select(i => SelectFaceByIndex(part, i)).ToList(); + + var entity = part.NewEntity((short)Obj3dType.o3d_shellOperation) as ksEntity + ?? throw new InvalidOperationException("NewEntity(o3d_shellOperation) вернул null."); + var def = entity.GetDefinition() as ksShellDefinition + ?? throw new InvalidOperationException("GetDefinition() оболочки вернул не ksShellDefinition."); + var arr = def.FaceArray() as ksEntityCollection + ?? throw new InvalidOperationException("FaceArray() оболочки вернул не ksEntityCollection."); + foreach (var f in faces) arr.Add(f); + def.thickness = thickness; + def.thinType = !outward; // true=внутрь, false=наружу + + 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.Core/Modeling/PartModeler.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs index fa534aa..353fb93 100644 --- a/src/Kompas.Mcp.Core/Modeling/PartModeler.cs +++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs @@ -80,6 +80,18 @@ public sealed partial class PartModeler : IDisposable ?? throw new InvalidOperationException("Ребро из коллекции не приводится к ksEntity."); } + /// Выбрать грань детали по индексу из list_faces (с проверкой диапазона). + private static ksEntity SelectFaceByIndex(ksPart part, int faceIndex) + { + var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection + ?? throw new InvalidOperationException("Не удалось получить коллекцию граней."); + if (faceIndex < 0 || faceIndex >= faces.GetCount()) + throw new ArgumentOutOfRangeException(nameof(faceIndex), + $"Индекс грани вне диапазона [0; {faces.GetCount() - 1}]. Сверьтесь с list_faces."); + return faces.GetByIndex(faceIndex) as ksEntity + ?? throw new InvalidOperationException("Грань по индексу не приводится к ksEntity."); + } + /// Выбрать ребро детали по индексу из list_edges. private static ksEntity SelectEdgeByIndex(ksPart part, int edgeIndex) { diff --git a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs index 0dfa09f..6b74327 100644 --- a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs +++ b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs @@ -91,6 +91,19 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler) return $"Фаска {distance}×{distance} создана на ребре [{edgeIndex}], id={id}."; } + [McpServerTool(Name = "shell")] + [Description("Превратить тело активной детали в оболочку (придать стенкам толщину): удалить (открыть) грани с индексами faceIndices из list_faces и задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется), true — наружу. Возвращает id операции.")] + public async Task Shell( + [Description("Индексы удаляемых (открываемых) граней из list_faces")] int[] faceIndices, + [Description("Толщина стенки, мм")] double thickness, + [Description("Толщина наружу (true) или внутрь (false)")] bool outward = false) + { + await session.ConnectAsync(); + var id = await modeler.ShellAsync(faceIndices, thickness, outward); + var removed = faceIndices?.Distinct().Count() ?? 0; + return $"Оболочка толщиной {thickness} мм создана (удалено граней: {removed}), 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 new file mode 100644 index 0000000..c102d9d --- /dev/null +++ b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs @@ -0,0 +1,51 @@ +using Kompas.Mcp.Core.Documents; +using Kompas.Mcp.Core.Modeling; +using Kompas.Mcp.Core.Query; + +namespace Kompas.Mcp.Tests.Integration; + +/// Интеграция: формообразующие операции пакета B. +[Trait("Category", "Integration")] +[Collection(KompasCollection.Name)] +public sealed class FeatureOpsTests +{ + private readonly DocumentService _docs; + private readonly PartModeler _modeler; + private readonly QueryService _query; + + public FeatureOpsTests(KompasFixture fx) + { + _docs = new DocumentService(fx.Session, fx.Dispatcher); + _modeler = new PartModeler(fx.Session, fx.Dispatcher); + _query = new QueryService(fx.Session, fx.Dispatcher); + } + + [Fact] + public async Task Shell_box_hollows_with_open_cap() + { + await _docs.CreateAsync(KompasDocumentType.Part); + try + { + // Коробка 40×30×20. + var s = await _modeler.OpenSketchAsync(BasePlane.XOY); + await _modeler.AddRectangleAsync(s, 0, 0, 40, 30); + await _modeler.CloseSketchAsync(s); + await _modeler.ExtrudeAsync(s, depth: 20); + var before = (await _query.GetPartInfoAsync()).Volume; // 24000 + + // Удаляем один из двух плоских торцов (площадь 40×30=1200) → открытая оболочка. + // Какой именно торец (верх/низ) — для объёма не важно: результат симметричен. + var faces = await _query.ListFacesAsync(); + var cap = faces.First(f => f.Type == "plane" && Math.Abs(f.Area - 1200) < 1); + var id = await _modeler.ShellAsync(new[] { cap.Index }, thickness: 2, outward: false); + Assert.True(id > 0); + await _modeler.RebuildAsync(); + + // Оболочка 2 мм с открытым торцом: 24000 − 36×26×18 = 7152 мм³. + var after = (await _query.GetPartInfoAsync()).Volume; + Assert.True(after < before, "После оболочки объём должен уменьшиться."); + Assert.InRange(after, 7152 * 0.95, 7152 * 1.05); + } + finally { await _docs.CloseAsync(save: false); } + } +}