Merge feat/shell: операция Оболочка (пакет B шаг 1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 22:36:29 +03:00
11 changed files with 391 additions and 18 deletions
+4 -3
View File
@@ -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<T>/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)
+3 -3
View File
@@ -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
+6 -5
View File
@@ -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`.
+3
View File
@@ -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`.
+7 -7
View File
@@ -128,7 +128,7 @@
<!-- HERO -->
<header class="hero">
<div class="wrap">
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы (50 инструментов)</span>
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell (51 инструмент)</span>
<h1>КОМПАС-3D <span class="g">MCP-сервер</span><br>управление CAD языком LLM</h1>
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
@@ -261,7 +261,7 @@
впереди.</p>
<div class="prog">
<div class="progrow"><span>Общий прогресс</span><span>v3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · навык kompas-3d · 50 инструментов · 78 тестов</span></div>
<div class="progrow"><span>Общий прогресс</span><span>v3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell (пакет B) · навык kompas-3d · 51 инструмент · 79 тестов</span></div>
<div class="bar"><i style="width:95%"></i></div>
</div>
@@ -297,18 +297,18 @@
<!-- TOOLS -->
<div class="panel">
<h3>Инструменты v3+ <span class="tag done">ГОТОВО</span></h3>
<p class="meta">50 инструментов, отдаются по MCP-протоколу</p>
<p class="meta">51 инструмент, отдаются по MCP-протоколу</p>
<ul class="list">
<li class="muted"><span class="mark ok"></span><span class="t"><code>System</code>: connect · status · set_visible</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Documents</code>: create · open · save · save_as · close · active</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Sketch</code>: create · on_face (точка/индекс) · add line/circle/arc/arc_3points/rectangle/axis/ellipse/polyline/polygon/spline/point · close</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Features</code>: extrude · revolve · fillet_edge/chamfer_edge (точка + индекс) · rebuild</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Features</code>: extrude · revolve · fillet_edge/chamfer_edge (точка + индекс) · <b>shell</b> (оболочка) · rebuild</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Edit</code>: move_face (прямое редактирование B-rep)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Inspection</code>: describe_model · list_features · list_bodies · list_variables · describe_face · describe_edge · measure</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Vision</code>: model_snapshot 👁️ (fallback)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Query</code>: get_part_info · get_bounding_box · list_faces · list_edges · list_components</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Conversion</code>: import_step · export_step</span></li>
<li><span class="mark todo"></span><span>Пакет B (операции) · пакет C (массивы) · рассечение/перемещение тела (далее)</span></li>
<li><span class="mark wip"></span><span>Пакет B (продолжение: rib/draft/loft/sweep/hole) · пакет C (массивы) · рассечение/перемещение тела</span></li>
</ul>
</div>
@@ -319,7 +319,7 @@
<ul class="list">
<li class="muted"><span class="mark ok"></span><span class="t">Unit-тесты — без COM: диспетчер, enum-маппинг, StepFormat, InspectionText</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Интеграционные тесты с КОМПАС: подключение, документы, снимок, цикл, STEP round-trip, сборка, move_face, инспекция</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Итого 78 тестов зелёных (47 unit + 31 integration)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Итого 79 тестов зелёных (47 unit + 32 integration)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Сборка <code>kompas-mcp.exe</code>, README с конфигом клиента</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Навык <code>kompas-3d</code> (методика) + полигон <code>usecases/</code></span></li>
<li><span class="mark todo"></span><span>In-process тест MCP-клиента, релизный publish</span></li>
@@ -341,7 +341,7 @@
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">v2 · готово ✓</div><h4>Операции, выборка, STEP</h4><p>Эскиз на грани · вращение · скругление/фаска · list_faces/edges · import_step · export_step · list_components · навык kompas-3d</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">v3 · готово ✓</div><h4>B-rep + структурный осмотр</h4><p><b>move_face</b> (сдвиг грани на N мм) · <b>describe_model</b> · list_features · list_bodies · list_variables · describe_face · describe_edge · measure</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">пакет A · готово ✓</div><h4>Богатые эскизы</h4><p>arc · arc_3points · ellipse · polyline · polygon · spline · point (+7 инструментов); <b>PartModeler</b> разбит на partial-классы; <b>SketchGeometry</b></p></div>
<div class="tl cur"><div class="ph">пакет B/C · в работе</div><h4>Операции + массивы</h4><p>Пакет B (формообразующие) · пакет C (массивы/паттерны) · рассечение тела; свойства документа</p></div>
<div class="tl cur"><div class="ph">пакет B · в работе</div><h4>Формообразующие операции</h4><p><b>shell</b> ✓ (оболочка) · далее: rib · draft · loft · sweep · hole · пакет C (массивы) · рассечение тела</p></div>
<div class="tl"><div class="ph">далее</div><h4>2D + сборки + транспорт</h4><p>Виды, размеры, штриховки · построение сборок, сопряжения; HTTP/SSE-транспорт</p></div>
</div>
</div>
+170
View File
@@ -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;
/// <summary>Интеграция: формообразующие операции пакета B.</summary>
[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
/// <summary>Выбрать грань детали по индексу из list_faces (с проверкой диапазона).</summary>
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
/// <summary>Превратить тело в оболочку: удалить (открыть) грани с индексами faceIndices,
/// задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется),
/// true — наружу. Возвращает id операции.</summary>
public Task<int> ShellAsync(IReadOnlyList<int> 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<string> 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`.
@@ -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<int> 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.
@@ -196,6 +196,40 @@ public sealed partial class PartModeler
return id;
}
/// <summary>Превратить тело в оболочку: удалить (открыть) грани с индексами faceIndices,
/// задать толщину стенки thickness (мм). outward=false — толщина внутрь (габарит сохраняется),
/// true — наружу. Возвращает id операции.</summary>
public Task<int> ShellAsync(IReadOnlyList<int> 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);
/// <summary>Перестроить документ.</summary>
public Task RebuildAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
@@ -80,6 +80,18 @@ public sealed partial class PartModeler : IDisposable
?? throw new InvalidOperationException("Ребро из коллекции не приводится к ksEntity.");
}
/// <summary>Выбрать грань детали по индексу из list_faces (с проверкой диапазона).</summary>
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.");
}
/// <summary>Выбрать ребро детали по индексу из list_edges.</summary>
private static ksEntity SelectEdgeByIndex(ksPart part, int edgeIndex)
{
+13
View File
@@ -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<string> 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<string> Rebuild()
@@ -0,0 +1,51 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Query;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: формообразующие операции пакета B.</summary>
[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); }
}
}