Merge feat/shell: операция Оболочка (пакет B шаг 1)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user