Merge feat/sketch-primitives: пакет A — богаче эскизы (7 примитивов)
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**.
|
||||
**43 MCP tools, 60 tests green.** 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. Latest: **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, МЦХ).
|
||||
**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`.
|
||||
See [`README.md`](README.md) and [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md); design decisions/caveats in
|
||||
[`docs/OPEN_QUESTIONS.md`](docs/OPEN_QUESTIONS.md).
|
||||
|
||||
@@ -52,11 +52,12 @@ Key implementation facts (don't relearn):
|
||||
- Logs go to **stderr** (stdout is the MCP channel). Integration tests reuse one КОМПАС via `KompasFixture`; artifacts land in gitignored `.scratch/`.
|
||||
- **Structural model inspection** (`ModelInspectionService`, `InspectionTools`): `describe_model` — one-call structural "passport" (bounding box + МЦХ + bodies + topology summary + feature tree + variables); preferred over snapshot. `list_features` — feature tree with params (depth/radius/legs). `list_bodies` — list solid/surface bodies with face count. `list_variables` — model variables (name/expression/value, external/info flags). `describe_face` — drill-down by index (type, area, normal, radius, edge count). `describe_edge` — drill-down by index (type, length, adjacent faces, vertices). `measure` — distance/angle between two objects (face|edge|vertex by index, unit `ST_MIX_MM`).
|
||||
- **Feature tree API** (don't relearn): read via API5 `ksPart.GetFeature()` → `(ksFeature).SubFeatureCollection(true,false)` → `ksFeatureCollection`. Cast `(ksFeature)part` does NOT work (RCW QI returns null) — must use `GetFeature()`. `ksFeature.type` returns only coarse `o3d_entity` and does NOT distinguish operations; precise type and params come from `ksFeature.GetObject()` → `ksEntity.GetDefinition()` and matching definition type (`ksBossExtrusionDefinition.GetSideParam`, `ksFilletDefinition.radius`, `ksChamferDefinition.GetChamferParam`, …). Node name ("Элемент выдавливания:1", "Скругление:1") is localized by КОМПАС itself.
|
||||
- **STEP import/export, `list_components`, `move_face`, and full inspection layer are done.** Not yet done: body split/reposition as MCP tools (SplitSolids/BodyRepositions — driveable but not yet implemented); patterns/arrays, 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, 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 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.
|
||||
- **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,13 +39,13 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe
|
||||
}
|
||||
```
|
||||
|
||||
## Инструменты (43 инструмента)
|
||||
## Инструменты (50 инструментов)
|
||||
|
||||
| Группа | Инструменты |
|
||||
|---|---|
|
||||
| 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_close` |
|
||||
| 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` |
|
||||
| Edit | `move_face` (прямое редактирование: сдвинуть грань на N мм вдоль нормали) |
|
||||
| Inspection | `describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure` |
|
||||
@@ -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/ 60 тестов: unit + integration (integration требуют КОМПАС)
|
||||
tests/Kompas.Mcp.Tests/ 78 тестов: unit + integration (integration требуют КОМПАС)
|
||||
libs/kompas-interop/ вендорские interop-сборки КОМПАС (из SDK Samples/Common)
|
||||
docs/ архитектура, план, презентация, MD-база знаний SDK (Kompas3D_SDK/)
|
||||
usecases/ полигон обкатки подходов (в .gitignore); приёмы поднимаются в навык kompas-3d
|
||||
|
||||
+15
-12
@@ -1,6 +1,6 @@
|
||||
# Архитектура MCP-сервера КОМПАС-3D (предлагаемый вариант)
|
||||
|
||||
> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection). Актуализировано по коду.
|
||||
> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection+package-A «богаче эскизы»). Актуализировано по коду.
|
||||
> Сопутствующий контекст по 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) + интеграционные (требуют КОМПАС). 60 тестов. |
|
||||
| **Kompas.Mcp.Tests** | Юнит (без COM) + интеграционные (требуют КОМПАС). 78 тестов (47 unit + 31 integration). |
|
||||
|
||||
Принцип: **только `Core` знает про COM**. `Host`/Tools оперируют доменными DTO и вызывают `Core`; `Host` не содержит бизнес-логики.
|
||||
|
||||
@@ -121,7 +121,7 @@ MCP client ──stdio──> Host (поток-пул, async)
|
||||
|
||||
---
|
||||
|
||||
## 5. Карта инструментов (43 инструмента)
|
||||
## 5. Карта инструментов (50 инструментов)
|
||||
|
||||
Сгруппированы вокруг центрального цикла «эскиз ↔ операция». Имена — `snake_case`.
|
||||
|
||||
@@ -136,10 +136,12 @@ MCP client ──stdio──> Host (поток-пул, async)
|
||||
|
||||
**Sketch** (2D-геометрия внутри 3D-эскиза)
|
||||
- `sketch_create` — на базовой плоскости (`o3d_planeXOY` / `XOZ` / `YOZ`) или на выбранной грани.
|
||||
- `sketch_add_line` / `sketch_add_circle` / `sketch_add_arc` / `sketch_add_rectangle`.
|
||||
- `sketch_add_line` / `sketch_add_circle` / `sketch_add_arc` / `sketch_add_arc_3points` / `sketch_add_rectangle`.
|
||||
- `sketch_add_ellipse` (centre, semi-axes A/B, rotation angle) / `sketch_add_polyline` (point chain, closed) / `sketch_add_polygon` (regular, inscribed/circumscribed) / `sketch_add_spline` (cubic NURBS, closed) / `sketch_add_point`.
|
||||
- `sketch_close` — завершить редактирование.
|
||||
- Жизненный цикл: `NewEntity(o3d_sketch)` → `SetPlane(plane|face)` → `BeginEdit()` →
|
||||
`ksLineSeg` / `ksCircle` / … → `EndEdit()`.
|
||||
`ksLineSeg` / `ksCircle` / `ksArcBy3Points` / `ksArcByAngle` / `ksEllipse` / `ksRegularPolygon` / `ksNurbs` / … → `EndEdit()`.
|
||||
- `PartModeler` split into partial classes: `PartModeler.cs` (core), `PartModeler.Sketch.cs`, `PartModeler.Features.cs`. Static `SketchGeometry` class holds mappings and validators. Point lists use `record SketchPoint(X, Y)` (JSON `x`/`y`). All angles in degrees.
|
||||
|
||||
**Features** (3D-операции)
|
||||
- `extrude_boss` / `extrude_cut` — sketch, depth, direction, endType.
|
||||
@@ -306,15 +308,16 @@ dotnet build -c Release -r win-x64 # сборка
|
||||
|
||||
## 10. Дорожная карта
|
||||
|
||||
**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection):** документы, эскизы, выдавливание/вырез, вращение, скругление/фаска, снимок; `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):** документы, эскизы (полный набор 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`** (структурный осмотр модели).
|
||||
|
||||
**Следующие приоритеты:**
|
||||
1. Рассечение/перемещение тела как MCP-инструменты (`SplitSolids`/`BodyRepositions`) — механика есть, продуктизация не закончена.
|
||||
2. Массивы (`ILinearPattern`, `ICircularPattern`).
|
||||
3. Свойства документа: `IPropertyMng` / `IPropertyKeeper`.
|
||||
4. Полноценное 2D-черчение: виды, линии/дуги/окружности, размеры (`ILinearDimension`, …), штриховки, тексты.
|
||||
5. Спецификации (`ISpecification`), построение сборок и сопряжения.
|
||||
6. Транспорт **HTTP/SSE** — как опция для удалённых клиентов.
|
||||
1. Пакет B «формообразующие» — дополнительные 3D-операции (уклоны, рёбра, оболочка и др.).
|
||||
2. Пакет C «массивы» — `ILinearPattern`, `ICircularPattern`.
|
||||
3. Рассечение/перемещение тела как MCP-инструменты (`SplitSolids`/`BodyRepositions`) — механика есть, продуктизация не закончена.
|
||||
4. Свойства документа: `IPropertyMng` / `IPropertyKeeper`.
|
||||
5. Полноценное 2D-черчение: виды, линии/дуги/окружности, размеры (`ILinearDimension`, …), штриховки, тексты.
|
||||
6. Спецификации (`ISpecification`), построение сборок и сопряжения.
|
||||
7. Транспорт **HTTP/SSE** — как опция для удалённых клиентов.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+16
-12
@@ -98,7 +98,7 @@
|
||||
.progrow{display:flex;justify-content:space-between;font-size:13px;color:var(--muted);margin-bottom:8px}
|
||||
|
||||
/* roadmap timeline */
|
||||
.timeline{display:grid;grid-template-columns:repeat(5,1fr);gap:14px;margin-top:8px}
|
||||
.timeline{display:grid;grid-template-columns:repeat(6,1fr);gap:14px;margin-top:8px}
|
||||
.tl{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:18px 16px;position:relative}
|
||||
.tl .ph{font-size:12px;font-weight:700;color:var(--accent)}
|
||||
.tl h4{font-size:15px;margin:8px 0 6px}
|
||||
@@ -110,6 +110,9 @@
|
||||
footer a:hover{text-decoration:underline}
|
||||
.links{display:flex;gap:20px;flex-wrap:wrap;margin-top:10px}
|
||||
|
||||
@media(max-width:1020px){
|
||||
.timeline{grid-template-columns:repeat(3,1fr)}
|
||||
}
|
||||
@media(max-width:880px){
|
||||
.grid,.stack{grid-template-columns:1fr 1fr}
|
||||
.todo-grid{grid-template-columns:1fr}
|
||||
@@ -125,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→структурный осмотр (43 инструмента)</span>
|
||||
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы (50 инструментов)</span>
|
||||
<h1>КОМПАС-3D <span class="g">MCP-сервер</span><br>управление CAD языком LLM</h1>
|
||||
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
|
||||
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
|
||||
@@ -258,8 +261,8 @@
|
||||
впереди.</p>
|
||||
|
||||
<div class="prog">
|
||||
<div class="progrow"><span>Общий прогресс</span><span>v3: STEP · list_components · move_face · структурный осмотр · навык kompas-3d · 43 инструмента · 60 тестов</span></div>
|
||||
<div class="bar"><i style="width:93%"></i></div>
|
||||
<div class="progrow"><span>Общий прогресс</span><span>v3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · навык kompas-3d · 50 инструментов · 78 тестов</span></div>
|
||||
<div class="bar"><i style="width:95%"></i></div>
|
||||
</div>
|
||||
|
||||
<div class="todo-grid" style="margin-top:26px">
|
||||
@@ -293,19 +296,19 @@
|
||||
|
||||
<!-- TOOLS -->
|
||||
<div class="panel">
|
||||
<h3>Инструменты v3 <span class="tag done">ГОТОВО</span></h3>
|
||||
<p class="meta">43 инструмента, отдаются по MCP-протоколу</p>
|
||||
<h3>Инструменты v3+ <span class="tag done">ГОТОВО</span></h3>
|
||||
<p class="meta">50 инструментов, отдаются по 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/rectangle/axis · close</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>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>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>Массивы/паттерны · рассечение/перемещение тела (далее)</span></li>
|
||||
<li><span class="mark todo"></span><span>Пакет B (операции) · пакет C (массивы) · рассечение/перемещение тела (далее)</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -316,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">Итого 60 тестов зелёных</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">Сборка <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>
|
||||
@@ -337,8 +340,9 @@
|
||||
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">v1 · готово ✓</div><h4>Цикл моделирования</h4><p>Документы, эскизы, выдавливание/вырез, снимок модели</p></div>
|
||||
<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 cur"><div class="ph">v4 · в работе</div><h4>Массивы + 2D</h4><p>Массивы/паттерны · рассечение тела · виды, размеры, штриховки; свойства документа</p></div>
|
||||
<div class="tl"><div class="ph">v5</div><h4>Сборки и транспорт</h4><p>Построение сборок, сопряжения; HTTP/SSE-транспорт</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"><div class="ph">далее</div><h4>2D + сборки + транспорт</h4><p>Виды, размеры, штриховки · построение сборок, сопряжения; HTTP/SSE-транспорт</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,940 @@
|
||||
# Sketch Primitives (пакет A) — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Добавить 7 MCP-инструментов эскиза (дуга ×2, эллипс, ломаная, правильный многоугольник, сплайн, точка) поверх существующего цикла «эскиз → операция».
|
||||
|
||||
**Architecture:** Новые методы — тонкие обёртки над `ksDocument2D` (API5) в открытом эскизе, по образцу существующих `AddLineAsync`/`AddCircleAsync`. `ellipse`/`polygon`/`spline` используют параметрические структуры, получаемые через `KompasObject.GetParamStruct(...)` и освобождаемые в `finally`. `PartModeler` разбивается на partial-файлы (ядро/эскизы/операции). Чистые маппинги и валидация вынесены в `SketchGeometry` для unit-тестов; геометрия проверяется интеграционно (профиль → extrude → объём).
|
||||
|
||||
**Tech Stack:** .NET 8 (net8.0-windows, x64), C#, COM API5 (`Kompas6API5`, `Kompas6Constants`), MCP SDK `ModelContextProtocol`, xUnit.
|
||||
|
||||
**Подтверждено рефлексией по `libs/kompas-interop/`** (сигнатуры — дословно):
|
||||
```
|
||||
Int32 ksArcBy3Points(Double x1, y1, x2, y2, x3, y3, Int32 style)
|
||||
Int32 ksArcByAngle(Double xc, yc, rad, f1, f2, Int16 direction, Int32 style) // углы в градусах
|
||||
Int32 ksEllipse(Object par) // par = ksEllipseParam { xc, yc, A, B, angle(град), style }
|
||||
Int32 ksRegularPolygon(Object par, Int16 centre) // par = ksRegularPolygonParam { count, xc, yc, ang(град), radius, describe, style }
|
||||
Int32 ksNurbs(Int16 degree, Boolean close, Int32 style)
|
||||
Int32 ksNurbsPoint(Object par) // par = ksNurbsPointParam { x, y, weight }
|
||||
Int32 ksEndObj()
|
||||
Int32 ksPoint(Double x, y, Int32 style)
|
||||
```
|
||||
Коды структур (`Kompas6Constants.StructType2DEnum`): `ko_EllipseParam=22`, `ko_RegularPolygonParam=92`, `ko_NurbsPointParam=18`. Стиль линии «основная» = `1`; стиль точки = `0`.
|
||||
|
||||
**Источники истины (во избежание расхождений):** Automation-семантику методов берём из `docs/Kompas3D_SDK/`; точные C#-имена свойств и типы — из рефлексии по `libs/kompas-interop/` (interop генерирует, например, `ksEllipseParam.A`/`.B` с заглавной — это канон для компиляции, регистр в C# важен). При конфликте имени побеждает interop.
|
||||
|
||||
**NURBS — порядок, не степень:** параметр `degree` метода `ksNurbs` — это *порядок* кривой (степень полинома + 1, диапазон 3..10). Кубический сплайн = порядок **4**. В плане используем константу `SplineOrder = 4`.
|
||||
|
||||
**Регистрация:** доп. DI не нужна — `PartModeler` уже синглтон в [Program.cs](../../../src/Kompas.Mcp.Host/Program.cs:24), инструменты подхватываются `WithToolsFromAssembly()`.
|
||||
|
||||
**Команды:**
|
||||
- Сборка: `dotnet build -c Release`
|
||||
- Unit: `dotnet test --filter "Category=Unit"`
|
||||
- Integration (нужен запущенный КОМПАС): `dotnet test --filter "Category=Integration"`
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Рефакторинг PartModeler в partial class
|
||||
|
||||
Чистое перемещение существующих методов по трём файлам — поведение не меняется. Регрессия = существующие тесты зелёные.
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.cs`
|
||||
- Create: `src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs`
|
||||
- Create: `src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs`
|
||||
|
||||
- [ ] **Step 1: Создать `PartModeler.Sketch.cs`** с этим заголовком и перенести в него (вырезать из `PartModeler.cs`) методы: `OpenSketchAsync`, `OpenSketchOnFaceAsync`, `OpenSketchOnFaceIndexAsync`, `CreateSketchOn`, `AddLineAsync`, `AddCircleAsync`, `AddRectangleAsync`, `AddAxisAsync`, `CloseSketchAsync` (тела без изменений).
|
||||
|
||||
```csharp
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants3D;
|
||||
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
|
||||
/// <summary>PartModeler: создание эскизов и добавление 2D-примитивов.</summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed partial class PartModeler
|
||||
{
|
||||
// (перенесённые методы эскизов)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Создать `PartModeler.Features.cs`** с этим заголовком и перенести в него (вырезать из `PartModeler.cs`) методы: `ExtrudeAsync`, `RevolveAsync`, `FilletEdgeAsync`, `FilletEdgeIndexAsync`, `CreateFillet`, `ChamferEdgeAsync`, `ChamferEdgeIndexAsync`, `CreateChamfer`, `RebuildAsync` (тела без изменений).
|
||||
|
||||
```csharp
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants3D;
|
||||
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
|
||||
/// <summary>PartModeler: формообразующие операции (выдавливание, вращение, скругление, фаска).</summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed partial class PartModeler
|
||||
{
|
||||
// (перенесённые методы операций)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: В `PartModeler.cs`** изменить объявление класса на `public sealed partial class PartModeler : IDisposable` и оставить только ядро: поля (`_session`, `_dispatcher`, `_nextId`, `_sketches`, `_features`), константы (`MainLineStyle`, `AxisLineStyle`), ctor, вложенный класс `SketchEntry`, helpers `ActiveDoc3D`, `GetTopPart`, `SelectEdgeByPoint`, `SelectEdgeByIndex`, `RequireSketch`, `RequireOpenSketch`, `CloseSketchCore`, `ResetCore`, `ReleaseCom`, `ResetAsync`, `Dispose`. Добавить общий helper для параметрических структур (используется в Task 5/7/8):
|
||||
|
||||
```csharp
|
||||
/// <summary>Создать параметрическую 2D-структуру через KompasObject (API5) и привести к интерфейсу.
|
||||
/// При неудачном приведении освобождает сырой RCW, чтобы не утекал COM-объект.</summary>
|
||||
private T NewParam<T>(Kompas6Constants.StructType2DEnum kind) where T : class
|
||||
{
|
||||
var raw = _session.Kompas.GetParamStruct((short)kind);
|
||||
if (raw is T typed) return typed;
|
||||
ReleaseCom(raw);
|
||||
throw new InvalidOperationException($"GetParamStruct({kind}) вернул не {typeof(T).Name}.");
|
||||
}
|
||||
```
|
||||
|
||||
Добавить в начало `PartModeler.cs` директиву `using Kompas6Constants;` (рядом с существующими `using`).
|
||||
|
||||
- [ ] **Step 4: Собрать**
|
||||
|
||||
Run: `dotnet build -c Release`
|
||||
Expected: Build succeeded, 0 ошибок.
|
||||
|
||||
- [ ] **Step 5: Регрессия — существующие интеграционные тесты модели**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~ModelingTests"`
|
||||
Expected: PASS (все тесты `ModelingTests` зелёные — рефакторинг ничего не сломал).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Kompas.Mcp.Core/Modeling/PartModeler.cs src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs
|
||||
git commit -m "refactor(core): PartModeler в partial class (ядро/эскизы/операции)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 2: SketchGeometry (чистые маппинги/валидация) + SketchPoint
|
||||
|
||||
**Files:**
|
||||
- Create: `src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs`
|
||||
- Create: `src/Kompas.Mcp.Host/Tools/SketchPoint.cs`
|
||||
- Test: `tests/Kompas.Mcp.Tests/SketchGeometryTests.cs`
|
||||
|
||||
- [ ] **Step 1: Написать падающий unit-тест**
|
||||
|
||||
```csharp
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
|
||||
namespace Kompas.Mcp.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class SketchGeometryTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true, 1)]
|
||||
[InlineData(false, -1)]
|
||||
public void ArcDirection_maps_orientation(bool ccw, int expected)
|
||||
=> Assert.Equal(expected, (int)SketchGeometry.ArcDirection(ccw));
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, false)] // вписанный → describe=false
|
||||
[InlineData(false, true)] // описанный → describe=true
|
||||
public void PolygonDescribe_inverts_inscribed(bool inscribed, bool expected)
|
||||
=> Assert.Equal(expected, SketchGeometry.PolygonDescribe(inscribed));
|
||||
|
||||
[Fact]
|
||||
public void RequirePositive_throws_on_zero_or_negative()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => SketchGeometry.RequirePositive(0, "radius"));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => SketchGeometry.RequirePositive(-1, "radius"));
|
||||
Assert.Null(Record.Exception(() => SketchGeometry.RequirePositive(0.1, "radius")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequireVertexCount_throws_below_three()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => SketchGeometry.RequireVertexCount(2));
|
||||
Assert.Null(Record.Exception(() => SketchGeometry.RequireVertexCount(3)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirePoints_throws_on_null()
|
||||
=> Assert.Throws<ArgumentNullException>(
|
||||
() => SketchGeometry.RequirePoints(null!, 2, "points"));
|
||||
|
||||
[Fact]
|
||||
public void RequirePoints_throws_when_too_few()
|
||||
=> Assert.Throws<ArgumentException>(
|
||||
() => SketchGeometry.RequirePoints(new[] { (0.0, 0.0) }, 2, "points"));
|
||||
|
||||
[Fact]
|
||||
public void RequirePoints_throws_on_non_finite()
|
||||
=> Assert.Throws<ArgumentException>(
|
||||
() => SketchGeometry.RequirePoints(new[] { (0.0, 0.0), (double.NaN, 1.0) }, 2, "points"));
|
||||
|
||||
[Fact]
|
||||
public void RequirePoints_passes_on_valid()
|
||||
=> Assert.Null(Record.Exception(
|
||||
() => SketchGeometry.RequirePoints(new[] { (0.0, 0.0), (1.0, 1.0) }, 2, "points")));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Запустить — убедиться, что не компилируется/падает**
|
||||
|
||||
Run: `dotnet test --filter "FullyQualifiedName~SketchGeometryTests"`
|
||||
Expected: FAIL (тип `SketchGeometry` не существует).
|
||||
|
||||
- [ ] **Step 3: Реализовать `SketchGeometry.cs`**
|
||||
|
||||
```csharp
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
|
||||
/// <summary>Чистые помощники геометрии эскиза: маппинги контракта в параметры COM и валидация.</summary>
|
||||
public static class SketchGeometry
|
||||
{
|
||||
/// <summary>Направление дуги для ksArcByAngle: против часовой → 1, по часовой → -1.</summary>
|
||||
public static short ArcDirection(bool counterClockwise) => (short)(counterClockwise ? 1 : -1);
|
||||
|
||||
/// <summary>describe для ksRegularPolygon: вписанный (вершины на окружности) → false; описанный → true.</summary>
|
||||
public static bool PolygonDescribe(bool inscribed) => !inscribed;
|
||||
|
||||
/// <summary>Требовать строго положительное значение (радиус, полуось).</summary>
|
||||
public static void RequirePositive(double value, string paramName)
|
||||
{
|
||||
if (!(value > 0))
|
||||
throw new ArgumentOutOfRangeException(paramName, value, "Значение должно быть > 0.");
|
||||
}
|
||||
|
||||
/// <summary>Требовать >= 3 вершин для правильного многоугольника.</summary>
|
||||
public static void RequireVertexCount(int count)
|
||||
{
|
||||
if (count < 3)
|
||||
throw new ArgumentOutOfRangeException(nameof(count), count, "Число вершин должно быть >= 3.");
|
||||
}
|
||||
|
||||
/// <summary>Валидировать список точек: не null, минимум <paramref name="min"/>, все координаты конечны.</summary>
|
||||
public static void RequirePoints(IReadOnlyList<(double x, double y)> points, int min, string paramName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(points, paramName);
|
||||
if (points.Count < min)
|
||||
throw new ArgumentException($"Нужно минимум {min} точек, передано {points.Count}.", paramName);
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
if (!double.IsFinite(points[i].x) || !double.IsFinite(points[i].y))
|
||||
throw new ArgumentException($"Точка [{i}] имеет неконечную координату.", paramName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Реализовать `SketchPoint.cs`** (тип параметра MCP для polyline/spline)
|
||||
|
||||
```csharp
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Kompas.Mcp.Host.Tools;
|
||||
|
||||
/// <summary>Точка эскиза в его плоскости (мм). Элемент списка для ломаной/сплайна.
|
||||
/// JSON-имена зафиксированы lowercase (x/y), чтобы схема инструмента совпадала с контрактом.</summary>
|
||||
public sealed record SketchPoint(
|
||||
[property: JsonPropertyName("x")][property: Description("Координата X в плоскости эскиза, мм")] double X,
|
||||
[property: JsonPropertyName("y")][property: Description("Координата Y в плоскости эскиза, мм")] double Y);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Запустить unit-тесты**
|
||||
|
||||
Run: `dotnet test --filter "FullyQualifiedName~SketchGeometryTests"`
|
||||
Expected: PASS (все тесты `SketchGeometryTests`).
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs src/Kompas.Mcp.Host/Tools/SketchPoint.cs tests/Kompas.Mcp.Tests/SketchGeometryTests.cs
|
||||
git commit -m "feat(core): SketchGeometry (маппинги/валидация) + SketchPoint"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 3: Дуга по трём точкам
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs`
|
||||
- Modify: `src/Kompas.Mcp.Host/Tools/SketchTools.cs`
|
||||
- Test: `tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs`
|
||||
|
||||
- [ ] **Step 1: Создать файл интеграционных тестов с падающим тестом**
|
||||
|
||||
```csharp
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
using Kompas.Mcp.Core.Query;
|
||||
|
||||
namespace Kompas.Mcp.Tests.Integration;
|
||||
|
||||
/// <summary>Интеграция: новые примитивы эскиза (профиль → выдавливание → объём).</summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Collection(KompasCollection.Name)]
|
||||
public sealed class SketchPrimitivesTests
|
||||
{
|
||||
private readonly DocumentService _docs;
|
||||
private readonly PartModeler _modeler;
|
||||
private readonly QueryService _query;
|
||||
|
||||
public SketchPrimitivesTests(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 Arc3Points_semicircle_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Верхний полукруг R10: дуга (10,0)→(0,10)→(-10,0) + хорда обратно.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddArc3PointsAsync(s, 10, 0, 0, 10, -10, 0);
|
||||
await _modeler.AddLineAsync(s, -10, 0, 10, 0);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·10²/2·5 ≈ 785.4
|
||||
var expected = Math.PI * 100 / 2 * 5;
|
||||
Assert.InRange(v, expected * 0.95, expected * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Запустить — убедиться, что падает**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Arc3Points_semicircle_extrudes"`
|
||||
Expected: FAIL (метод `AddArc3PointsAsync` не существует).
|
||||
|
||||
- [ ] **Step 3: Реализовать метод модели** (в `PartModeler.Sketch.cs`)
|
||||
|
||||
```csharp
|
||||
/// <summary>Добавить дугу по трём точкам (начало, точка на дуге, конец) в открытый эскиз.</summary>
|
||||
public Task AddArc3PointsAsync(int sketchId, double x1, double y1, double x2, double y2, double x3, double y3, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksArcBy3Points(x1, y1, x2, y2, x3, y3, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksArcBy3Points вернул 0 (дуга не создана — три точки коллинеарны?).");
|
||||
}, ct);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Реализовать инструмент** (в `SketchTools.cs`, перед `Close`)
|
||||
|
||||
```csharp
|
||||
[McpServerTool(Name = "sketch_add_arc_3points")]
|
||||
[Description("Добавить дугу по трём точкам (начало, промежуточная точка на дуге, конец) в открытый эскиз. Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddArc3Points(int sketchId, double x1, double y1, double x2, double y2, double x3, double y3)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddArc3PointsAsync(sketchId, x1, y1, x2, y2, x3, y3);
|
||||
return "Дуга по 3 точкам добавлена.";
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Запустить тест**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Arc3Points_semicircle_extrudes"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
|
||||
git commit -m "feat(sketch): дуга по 3 точкам (sketch_add_arc_3points)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Дуга по центру и углам
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs`
|
||||
- Modify: `src/Kompas.Mcp.Host/Tools/SketchTools.cs`
|
||||
- Test: `tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs`
|
||||
|
||||
- [ ] **Step 1: Добавить падающий тест** (в `SketchPrimitivesTests`)
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task ArcByAngle_quarter_sector_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Сектор 90° R10 в первом квадранте: дуга (10,0)→(0,10) + два радиуса к центру.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddArcByAngleAsync(s, 0, 0, 10, 0, 90, counterClockwise: true);
|
||||
await _modeler.AddLineAsync(s, 0, 10, 0, 0);
|
||||
await _modeler.AddLineAsync(s, 0, 0, 10, 0);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·10²/4·5 ≈ 392.7
|
||||
var expected = Math.PI * 100 / 4 * 5;
|
||||
Assert.InRange(v, expected * 0.95, expected * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Запустить — убедиться, что падает**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.ArcByAngle_quarter_sector_extrudes"`
|
||||
Expected: FAIL (метод `AddArcByAngleAsync` не существует).
|
||||
|
||||
- [ ] **Step 3: Реализовать метод модели** (в `PartModeler.Sketch.cs`)
|
||||
|
||||
```csharp
|
||||
/// <summary>Добавить дугу по центру, радиусу и углам (градусы) в открытый эскиз.</summary>
|
||||
public Task AddArcByAngleAsync(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequirePositive(radius, nameof(radius));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
var dir = SketchGeometry.ArcDirection(counterClockwise);
|
||||
if (editor.ksArcByAngle(centerX, centerY, radius, startAngle, endAngle, dir, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksArcByAngle вернул 0 (дуга не создана).");
|
||||
}, ct);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Реализовать инструмент** (в `SketchTools.cs`)
|
||||
|
||||
```csharp
|
||||
[McpServerTool(Name = "sketch_add_arc")]
|
||||
[Description("Добавить дугу по центру, радиусу и углам (в градусах, от оси X) в открытый эскиз. counterClockwise=true — против часовой стрелки, false — по часовой. Координаты центра — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddArc(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddArcByAngleAsync(sketchId, centerX, centerY, radius, startAngle, endAngle, counterClockwise);
|
||||
return "Дуга добавлена.";
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Запустить тест**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.ArcByAngle_quarter_sector_extrudes"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
|
||||
git commit -m "feat(sketch): дуга по центру и углам (sketch_add_arc)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Эллипс
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs`
|
||||
- Modify: `src/Kompas.Mcp.Host/Tools/SketchTools.cs`
|
||||
- Test: `tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs`
|
||||
|
||||
- [ ] **Step 1: Добавить падающий тест**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Ellipse_extrudes_to_expected_volume()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddEllipseAsync(s, 0, 0, semiMajor: 10, semiMinor: 5);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 4);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·10·5·4 ≈ 628.3
|
||||
var expected = Math.PI * 10 * 5 * 4;
|
||||
Assert.InRange(v, expected * 0.97, expected * 1.03);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Запустить — убедиться, что падает**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Ellipse_extrudes_to_expected_volume"`
|
||||
Expected: FAIL (метод `AddEllipseAsync` не существует).
|
||||
|
||||
- [ ] **Step 3: Реализовать метод модели** (в `PartModeler.Sketch.cs`). Использует `NewParam<T>` из Task 1; требует `using Kompas6API5;` (уже есть в файле).
|
||||
|
||||
```csharp
|
||||
/// <summary>Добавить эллипс: центр, полуоси A/B (мм), угол наклона большой оси к X (градусы).</summary>
|
||||
public Task AddEllipseAsync(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequirePositive(semiMajor, nameof(semiMajor));
|
||||
SketchGeometry.RequirePositive(semiMinor, nameof(semiMinor));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
ksEllipseParam? p = null;
|
||||
try
|
||||
{
|
||||
p = NewParam<ksEllipseParam>(Kompas6Constants.StructType2DEnum.ko_EllipseParam);
|
||||
// ВНИМАНИЕ: interop генерирует свойства A/B с ЗАГЛАВНОЙ (не a/b как в SDK-доках). Регистр в C# важен.
|
||||
p.xc = centerX; p.yc = centerY;
|
||||
p.A = semiMajor; p.B = semiMinor;
|
||||
p.angle = angle; p.style = MainLineStyle;
|
||||
if (editor.ksEllipse(p) == 0)
|
||||
throw new InvalidOperationException("ksEllipse вернул 0 (эллипс не создан).");
|
||||
}
|
||||
finally { ReleaseCom(p); }
|
||||
}, ct);
|
||||
```
|
||||
|
||||
Добавить в начало `PartModeler.Sketch.cs` директиву `using Kompas6Constants;`.
|
||||
|
||||
- [ ] **Step 4: Реализовать инструмент** (в `SketchTools.cs`)
|
||||
|
||||
```csharp
|
||||
[McpServerTool(Name = "sketch_add_ellipse")]
|
||||
[Description("Добавить эллипс в открытый эскиз: центр, большая полуось semiMajor, малая полуось semiMinor (мм), угол наклона большой оси к X (градусы, по умолчанию 0).")]
|
||||
public async Task<string> AddEllipse(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddEllipseAsync(sketchId, centerX, centerY, semiMajor, semiMinor, angle);
|
||||
return "Эллипс добавлен.";
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Запустить тест**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Ellipse_extrudes_to_expected_volume"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
|
||||
git commit -m "feat(sketch): эллипс (sketch_add_ellipse)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 6: Ломаная (polyline)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs`
|
||||
- Modify: `src/Kompas.Mcp.Host/Tools/SketchTools.cs`
|
||||
- Test: `tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs`
|
||||
|
||||
- [ ] **Step 1: Добавить падающий тест**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Polyline_closed_triangle_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Прямоугольный треугольник (0,0)-(20,0)-(0,15), площадь 150.
|
||||
var pts = new (double x, double y)[] { (0, 0), (20, 0), (0, 15) };
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddPolylineAsync(s, pts, closed: true);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // 150·5 = 750
|
||||
Assert.InRange(v, 750 * 0.97, 750 * 1.03);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Запустить — убедиться, что падает**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Polyline_closed_triangle_extrudes"`
|
||||
Expected: FAIL (метод `AddPolylineAsync` не существует).
|
||||
|
||||
- [ ] **Step 3: Реализовать метод модели** (в `PartModeler.Sketch.cs`)
|
||||
|
||||
```csharp
|
||||
/// <summary>Добавить ломаную (цепочку отрезков) по списку точек. closed замыкает последнюю с первой.</summary>
|
||||
public Task AddPolylineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
// closed-ломаная из 2 точек не образует площадь — требуем минимум 3.
|
||||
SketchGeometry.RequirePoints(points, closed ? 3 : 2, nameof(points));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
for (int i = 0; i + 1 < points.Count; i++)
|
||||
Seg(editor, points[i], points[i + 1]);
|
||||
if (closed)
|
||||
Seg(editor, points[^1], points[0]);
|
||||
|
||||
static void Seg(ksDocument2D ed, (double x, double y) a, (double x, double y) b)
|
||||
{
|
||||
if (ed.ksLineSeg(a.x, a.y, b.x, b.y, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksLineSeg вернул 0 (сегмент ломаной не создан).");
|
||||
}
|
||||
}, ct);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Реализовать инструмент** (в `SketchTools.cs`). Добавить также приватный helper `Map` (используется и для сплайна в Task 8).
|
||||
|
||||
```csharp
|
||||
[McpServerTool(Name = "sketch_add_polyline")]
|
||||
[Description("Добавить ломаную — цепочку прямых отрезков по списку точек — в открытый эскиз. closed=true замыкает последнюю точку с первой (тогда нужно минимум 3 точки). Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddPolyline(int sketchId, SketchPoint[] points, bool closed = false)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddPolylineAsync(sketchId, Map(points), closed);
|
||||
return $"Ломаная из {points.Length} точек добавлена{(closed ? " (замкнута)" : "")}.";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<(double x, double y)> Map(SketchPoint[] points)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(points);
|
||||
return points.Select(p => (p.X, p.Y)).ToList();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Запустить тест**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Polyline_closed_triangle_extrudes"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
|
||||
git commit -m "feat(sketch): ломаная по списку точек (sketch_add_polyline)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Правильный многоугольник
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs`
|
||||
- Modify: `src/Kompas.Mcp.Host/Tools/SketchTools.cs`
|
||||
- Test: `tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs`
|
||||
|
||||
- [ ] **Step 1: Добавить падающий тест**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Polygon_hexagon_extrudes_to_expected_volume()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Правильный 6-угольник, вписанный в окружность R10: площадь = 0.5·6·R²·sin(60°) ≈ 259.8.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddPolygonAsync(s, 0, 0, vertexCount: 6, radius: 10, inscribed: true);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume;
|
||||
var area = 0.5 * 6 * 100 * Math.Sin(2 * Math.PI / 6);
|
||||
Assert.InRange(v, area * 5 * 0.95, area * 5 * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Запустить — убедиться, что падает**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Polygon_hexagon_extrudes_to_expected_volume"`
|
||||
Expected: FAIL (метод `AddPolygonAsync` не существует).
|
||||
|
||||
- [ ] **Step 3: Реализовать метод модели** (в `PartModeler.Sketch.cs`)
|
||||
|
||||
```csharp
|
||||
/// <summary>Добавить правильный многоугольник: центр, vertexCount вершин, radius (мм),
|
||||
/// inscribed=true — вершины на окружности (вписанный), false — стороны касаются (описанный),
|
||||
/// angle — поворот первой вершины (градусы).</summary>
|
||||
public Task AddPolygonAsync(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequireVertexCount(vertexCount);
|
||||
SketchGeometry.RequirePositive(radius, nameof(radius));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
ksRegularPolygonParam? p = null;
|
||||
try
|
||||
{
|
||||
p = NewParam<ksRegularPolygonParam>(Kompas6Constants.StructType2DEnum.ko_RegularPolygonParam);
|
||||
p.count = vertexCount; p.xc = centerX; p.yc = centerY;
|
||||
p.radius = radius; p.ang = angle;
|
||||
p.describe = SketchGeometry.PolygonDescribe(inscribed);
|
||||
p.style = MainLineStyle;
|
||||
if (editor.ksRegularPolygon(p, 0) == 0)
|
||||
throw new InvalidOperationException("ksRegularPolygon вернул 0 (многоугольник не создан).");
|
||||
}
|
||||
finally { ReleaseCom(p); }
|
||||
}, ct);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Реализовать инструмент** (в `SketchTools.cs`)
|
||||
|
||||
```csharp
|
||||
[McpServerTool(Name = "sketch_add_polygon")]
|
||||
[Description("Добавить правильный многоугольник в открытый эскиз: центр, vertexCount вершин (>=3), radius (мм). inscribed=true — вершины лежат на окружности радиуса (вписанный в окружность); false — стороны касаются окружности (описанный). angle — поворот первой вершины (градусы, по умолчанию 0).")]
|
||||
public async Task<string> AddPolygon(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddPolygonAsync(sketchId, centerX, centerY, vertexCount, radius, inscribed, angle);
|
||||
return $"Правильный многоугольник ({vertexCount} вершин) добавлен.";
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Запустить тест**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Polygon_hexagon_extrudes_to_expected_volume"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
|
||||
git commit -m "feat(sketch): правильный многоугольник (sketch_add_polygon)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 8: Сплайн (NURBS)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs`
|
||||
- Modify: `src/Kompas.Mcp.Host/Tools/SketchTools.cs`
|
||||
- Test: `tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs`
|
||||
|
||||
- [ ] **Step 1: Добавить падающий тест**
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Spline_closed_loop_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Замкнутый сплайн по 4 точкам вокруг начала координат → выпуклая «капля».
|
||||
var pts = new (double x, double y)[] { (10, 0), (0, 10), (-10, 0), (0, -10) };
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddSplineAsync(s, pts, closed: true);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
// Замкнутый кубический сплайн через 4 точки даёт гладкий вогнутый контур, вписанный
|
||||
// в габаритный квадрат 20×20 (площадь профиля ≈135 мм²). Проверяем, что тело построено
|
||||
// и объём положителен и не превышает габарит профиля (площадь ≤ 400 → V ≤ 2000).
|
||||
var v = (await _query.GetPartInfoAsync()).Volume;
|
||||
Assert.InRange(v, 50 * 5, 400 * 5);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Запустить — убедиться, что падает**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Spline_closed_loop_extrudes"`
|
||||
Expected: FAIL (метод `AddSplineAsync` не существует).
|
||||
|
||||
- [ ] **Step 3: Реализовать метод модели** (в `PartModeler.Sketch.cs`). Добавить рядом приватную константу степени.
|
||||
|
||||
```csharp
|
||||
private const short SplineOrder = 4; // порядок NURBS (степень+1); 4 = кубический сплайн
|
||||
|
||||
/// <summary>Добавить сплайн (кубический NURBS, порядок 4) через список точек. closed замыкает кривую.</summary>
|
||||
public Task AddSplineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequirePoints(points, 2, nameof(points));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksNurbs(SplineOrder, closed, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksNurbs вернул 0 (сплайн не открыт).");
|
||||
// ksNurbs открыл составной объект — ksEndObj ОБЯЗАТЕЛЕН в любом исходе, иначе редактор «застрянет».
|
||||
var completed = false;
|
||||
try
|
||||
{
|
||||
foreach (var (x, y) in points)
|
||||
{
|
||||
ksNurbsPointParam? np = null;
|
||||
try
|
||||
{
|
||||
np = NewParam<ksNurbsPointParam>(Kompas6Constants.StructType2DEnum.ko_NurbsPointParam);
|
||||
np.x = x; np.y = y; np.weight = 1.0;
|
||||
if (editor.ksNurbsPoint(np) == 0)
|
||||
throw new InvalidOperationException("ksNurbsPoint вернул 0 (узел сплайна не добавлен).");
|
||||
}
|
||||
finally { ReleaseCom(np); }
|
||||
}
|
||||
completed = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Закрываем составной объект всегда; ошибку завершения сообщаем только на успешном пути,
|
||||
// чтобы не подменить исходное исключение из цикла.
|
||||
var end = editor.ksEndObj();
|
||||
if (completed && end == 0)
|
||||
throw new InvalidOperationException("ksEndObj вернул 0 (сплайн не завершён).");
|
||||
}
|
||||
}, ct);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Реализовать инструмент** (в `SketchTools.cs`)
|
||||
|
||||
```csharp
|
||||
[McpServerTool(Name = "sketch_add_spline")]
|
||||
[Description("Добавить сплайн (кубический NURBS) через список точек в открытый эскиз. closed=true замыкает кривую. Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddSpline(int sketchId, SketchPoint[] points, bool closed = false)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddSplineAsync(sketchId, Map(points), closed);
|
||||
return $"Сплайн по {points.Length} точкам добавлен{(closed ? " (замкнут)" : "")}.";
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Запустить тест**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Spline_closed_loop_extrudes"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
|
||||
git commit -m "feat(sketch): сплайн NURBS по списку точек (sketch_add_spline)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Вспомогательная точка
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs`
|
||||
- Modify: `src/Kompas.Mcp.Host/Tools/SketchTools.cs`
|
||||
- Test: `tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs`
|
||||
|
||||
- [ ] **Step 1: Добавить падающий тест** (smoke: `AddPointAsync` сам бросает при `ksPoint==0`, поэтому тест доказывает успешный вызов; плюс проверяем, что точка не ломает соседний контур)
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public async Task Point_does_not_break_sketch()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Окружность R10 + точка в центре; выдавливание должно дать цилиндр без искажений.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddCircleAsync(s, 0, 0, 10);
|
||||
await _modeler.AddPointAsync(s, 0, 0);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·100·5 ≈ 1570.8
|
||||
var expected = Math.PI * 100 * 5;
|
||||
Assert.InRange(v, expected * 0.97, expected * 1.03);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Запустить — убедиться, что падает**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Point_does_not_break_sketch"`
|
||||
Expected: FAIL (метод `AddPointAsync` не существует).
|
||||
|
||||
- [ ] **Step 3: Реализовать метод модели** (в `PartModeler.Sketch.cs`). Добавить рядом приватную константу стиля точки.
|
||||
|
||||
```csharp
|
||||
private const int PointStyle = 0; // системный стиль точки (см. SDK pstyles)
|
||||
|
||||
/// <summary>Добавить точку в открытый эскиз (опорная точка для построений).</summary>
|
||||
public Task AddPointAsync(int sketchId, double x, double y, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksPoint(x, y, PointStyle) == 0)
|
||||
throw new InvalidOperationException("ksPoint вернул 0 (точка не создана).");
|
||||
}, ct);
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Реализовать инструмент** (в `SketchTools.cs`)
|
||||
|
||||
```csharp
|
||||
[McpServerTool(Name = "sketch_add_point")]
|
||||
[Description("Добавить точку в открытый эскиз (опорная точка для построений). Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddPoint(int sketchId, double x, double y)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddPointAsync(sketchId, x, y);
|
||||
return "Точка добавлена.";
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Запустить тест**
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration&FullyQualifiedName~SketchPrimitivesTests.Point_does_not_break_sketch"`
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add src/Kompas.Mcp.Core/Modeling/PartModeler.Sketch.cs src/Kompas.Mcp.Host/Tools/SketchTools.cs tests/Kompas.Mcp.Tests/Integration/SketchPrimitivesTests.cs
|
||||
git commit -m "feat(sketch): вспомогательная точка (sketch_add_point)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task 10: Полный прогон и документация
|
||||
|
||||
**Files:**
|
||||
- Modify: `CLAUDE.md`, `README.md`, `docs/ARCHITECTURE.md` (через навык `docs-delegate`)
|
||||
|
||||
- [ ] **Step 1: Полный прогон unit-тестов**
|
||||
|
||||
Run: `dotnet test --filter "Category=Unit"`
|
||||
Expected: PASS (включая 4 новых `SketchGeometryTests`).
|
||||
|
||||
- [ ] **Step 2: Полный прогон интеграционных тестов** (нужен запущенный КОМПАС)
|
||||
|
||||
Run: `dotnet test --filter "Category=Integration"`
|
||||
Expected: PASS (включая 7 новых `SketchPrimitivesTests` + регрессия `ModelingTests`).
|
||||
|
||||
- [ ] **Step 3: Обновить документацию через навык `docs-delegate`**
|
||||
|
||||
Передать Sonnet задачу: увеличить счётчик инструментов на +7 и тестов на +11 (4 unit + 7 integration) в `CLAUDE.md` (раздел «Current state») — сверив текущие числа по факту; добавить 7 примитивов эскиза (дуга ×2, эллипс, ломаная, многоугольник, сплайн, точка) в описание sketch-слоя в `CLAUDE.md`, `README.md` и `docs/ARCHITECTURE.md`; отметить пакет A («богаче эскизы») выполненным. Не трогать `docs/OPEN_QUESTIONS.md` сверх упоминания, что примитивы эскиза добавлены.
|
||||
|
||||
- [ ] **Step 4: Commit документации**
|
||||
|
||||
```bash
|
||||
git add CLAUDE.md README.md docs/ARCHITECTURE.md
|
||||
git commit -m "docs: 7 новых примитивов эскиза (пакет A) в README/ARCHITECTURE/CLAUDE"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Завершение ветки** — использовать навык `superpowers:finishing-a-development-branch` для выбора merge/PR.
|
||||
|
||||
---
|
||||
|
||||
## Сводка контракта (для сверки типов между задачами)
|
||||
|
||||
Методы `PartModeler` (все `public Task ... Async(..., CancellationToken ct = default)`):
|
||||
- `AddArc3PointsAsync(int sketchId, double x1, y1, x2, y2, x3, y3)`
|
||||
- `AddArcByAngleAsync(int sketchId, double centerX, centerY, radius, startAngle, endAngle, bool counterClockwise = true)`
|
||||
- `AddEllipseAsync(int sketchId, double centerX, centerY, semiMajor, semiMinor, double angle = 0)`
|
||||
- `AddPolylineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false)`
|
||||
- `AddPolygonAsync(int sketchId, double centerX, centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0)`
|
||||
- `AddSplineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false)`
|
||||
- `AddPointAsync(int sketchId, double x, double y)`
|
||||
|
||||
`SketchGeometry` (public static): `short ArcDirection(bool)`, `bool PolygonDescribe(bool)`, `void RequirePositive(double, string)`, `void RequireVertexCount(int)`, `void RequirePoints(IReadOnlyList<(double x, double y)>, int, string)`.
|
||||
|
||||
`SketchPoint` (public record, Host): `record SketchPoint(double X, double Y)`.
|
||||
|
||||
MCP-инструменты (имена): `sketch_add_arc_3points`, `sketch_add_arc`, `sketch_add_ellipse`, `sketch_add_polyline`, `sketch_add_polygon`, `sketch_add_spline`, `sketch_add_point`.
|
||||
@@ -0,0 +1,137 @@
|
||||
# Дизайн: пакет A — богаче эскизы (примитивы 2D)
|
||||
|
||||
**Дата:** 2026-05-26
|
||||
**Статус:** одобрен к реализации
|
||||
**Контекст:** kompas3d-mcp — MCP-сервер для КОМПАС-3D через COM API (.NET 8, C#).
|
||||
|
||||
## Цель
|
||||
|
||||
Расширить набор примитивов эскиза. Сейчас профиль ограничен отрезком, окружностью,
|
||||
прямоугольником и осевой линией (`sketch_add_line/circle/rectangle/axis`) — без дуг и
|
||||
кривых недоступна масса реальных профилей. Добавляем **7 инструментов**: дугу (двумя
|
||||
способами), эллипс, ломаную, правильный многоугольник, сплайн и точку.
|
||||
|
||||
Это первый из приоритизированных пакетов расширения MCP (см. анализ функций от 2026-05-26:
|
||||
пакеты A — эскизы, B — формообразующие, C — массивы, D — параметрика). Выбран пакет A.
|
||||
|
||||
## Не входит в объём (YAGNI)
|
||||
|
||||
- Параметрические ограничения и размеры эскиза (отдельный крупный пакет).
|
||||
- Скругление/фаска углов эскиза (`pCorner` у многоугольника не используем).
|
||||
- Выставление порядка сплайна наружу — фиксируем кубический (порядок 4).
|
||||
- Дополнительные кривые (`ksArcByPoint`, дуга эллипса, Bezier, trim) — в backlog «пакет A2».
|
||||
|
||||
## Сигнатуры COM (Automation — по `docs/Kompas3D_SDK/`; типы — по рефлексии interop)
|
||||
|
||||
Все методы — на интерфейсе `ksDocument2D` (API5), вызываются на редакторе открытого
|
||||
эскиза (`def.BeginEdit()` → `ksDocument2D`). Углы **в градусах**. Стиль линии `1` =
|
||||
«основная» (`ksCSNormal`). Каждый метод возвращает `long`: `0` — ошибка.
|
||||
|
||||
```
|
||||
long ksArcBy3Points(double x1,y1, x2,y2, x3,y3, long style)
|
||||
long ksArcByAngle(double xc, yc, rad, f1_град, f2_град, short direction(1=CCW|-1=CW), long style)
|
||||
long ksEllipse(LPDISPATCH ksEllipseParam) // interop-свойства: xc,yc,A,B,angle(град),style
|
||||
long ksRegularPolygon(LPDISPATCH ksRegularPolygonParam, short centre=0)
|
||||
// count,xc,yc,ang(град),radius,describe(BOOL),style
|
||||
long ksNurbs(short degree, BOOL close, long style) // degree = ПОРЯДОК (степень+1); 4 = кубический
|
||||
long ksNurbsPoint(LPDISPATCH ksNurbsPointParam) // x,y,weight=1.0 — на каждый узел
|
||||
long ksEndObj() // завершить NURBS
|
||||
long ksPoint(double x, y, long style) // style=0
|
||||
long ksLineSeg(double x1,y1, x2,y2, long style) // уже используется — для polyline
|
||||
```
|
||||
|
||||
> **Источники истины.** Automation-семантика методов — из `docs/Kompas3D_SDK/`. Точные
|
||||
> C#-имена/типы свойств — из рефлексии по `libs/kompas-interop/`. Где interop расходится со
|
||||
> справкой, побеждает interop (компиляция регистрозависима): в частности, `ksEllipseParam`
|
||||
> имеет свойства **`A`/`B`** с заглавной (в справке — `a`/`b`). NURBS `degree` — это *порядок*
|
||||
> кривой (степень+1), кубический сплайн = порядок **4**.
|
||||
|
||||
**Param-структуры** (`ellipse`/`polygon`/`spline`) берутся через
|
||||
`KompasObject.GetParamStruct(...)` (`_session.Kompas`), заполняются, передаются в метод и
|
||||
**освобождаются** `ComHelper.Release` — по дисциплине COM-lifetime проекта. Это новый для
|
||||
`PartModeler` паттерн (раньше — только прямые методы вроде `ksLineSeg`/`ksCircle`).
|
||||
|
||||
Стиль точки: `0` (системный стиль точки, см. SDK `pstyles`).
|
||||
|
||||
## Новые MCP-инструменты (группа Sketch)
|
||||
|
||||
Контракт инструментов (обёртки в `SketchTools`, как существующие `sketch_add_*`;
|
||||
возвращают строку-подтверждение, id не возвращают):
|
||||
|
||||
| Инструмент | Параметры | COM |
|
||||
|---|---|---|
|
||||
| `sketch_add_arc_3points` | `sketchId, x1,y1, x2,y2, x3,y3` | `ksArcBy3Points(..., 1)` |
|
||||
| `sketch_add_arc` | `sketchId, centerX, centerY, radius, startAngle, endAngle, counterClockwise=true` | `ksArcByAngle(xc,yc,rad,f1,f2,dir,1)` |
|
||||
| `sketch_add_ellipse` | `sketchId, centerX, centerY, semiMajor, semiMinor, angle=0` | `ksEllipse(param)` |
|
||||
| `sketch_add_polyline` | `sketchId, points:[{x,y}…], closed=false` | цепочка `ksLineSeg(...,1)` |
|
||||
| `sketch_add_polygon` | `sketchId, centerX, centerY, vertexCount, radius, inscribed=true, angle=0` | `ksRegularPolygon(param, 0)` |
|
||||
| `sketch_add_spline` | `sketchId, points:[{x,y}…], closed=false` | `ksNurbs(4,close,1)` → `ksNurbsPoint`×N → `ksEndObj()` |
|
||||
| `sketch_add_point` | `sketchId, x, y` | `ksPoint(x,y,0)` |
|
||||
|
||||
### Маппинги контракт → COM
|
||||
|
||||
- `counterClockwise=true → direction=1`, `false → direction=-1`.
|
||||
- `inscribed=true → describe=false` (вершины на окружности `radius`);
|
||||
`inscribed=false → describe=true` (стороны касаются окружности).
|
||||
- `closed → ksNurbs(close=TRUE)` / для polyline замыкающий `ksLineSeg` от последней к первой.
|
||||
- Сплайн: `order=4` (кубический), на каждый узел `ksNurbsPoint` с `weight=1.0`; узловой вектор
|
||||
не задаём (КОМПАС строит автоматически).
|
||||
|
||||
### Формат списка точек
|
||||
|
||||
`points` — массив объектов `{x, y}` (double). JSON-имена зафиксированы lowercase через
|
||||
`[JsonPropertyName("x"/"y")]` на record `SketchPoint(double X, double Y)`, чтобы схема
|
||||
инструмента совпадала с контрактом. Координаты — в плоскости эскиза, мм.
|
||||
|
||||
## Валидация (до COM-вызова)
|
||||
|
||||
Вынесена в чистые статические guard'ы `SketchGeometry` (unit-тестируемы без COM); бросают
|
||||
`ArgumentException`/`ArgumentOutOfRangeException`/`ArgumentNullException`:
|
||||
- `radius > 0`, `semiMajor > 0`, `semiMinor > 0`;
|
||||
- `vertexCount >= 3`;
|
||||
- список точек: не `null`, все координаты конечны (`double.IsFinite`), и минимум:
|
||||
`polyline` — 2 (а при `closed=true` — 3, иначе площадь не образуется); `spline` — 2.
|
||||
|
||||
Возврат COM-метода `0` (или `ksEndObj() == 0` для сплайна) → `InvalidOperationException`
|
||||
с понятным текстом (паттерн `ksLineSeg вернул 0`).
|
||||
|
||||
## Структура кода
|
||||
|
||||
`PartModeler` (сейчас 443 строки) совмещает эскизы и операции; 7 новых методов раздуют
|
||||
эскизную часть. Разбиваем на **partial class**, сохраняя публичный класс, DI и общий
|
||||
реестр id:
|
||||
|
||||
- `PartModeler.cs` — поля, реестры `_sketches`/`_features`, helpers (`GetTopPart`,
|
||||
`RequireSketch`/`RequireOpenSketch`, `SelectEdgeBy*`), `ResetCore`/`Dispose`.
|
||||
- `PartModeler.Sketch.cs` — создание эскиза (`OpenSketch*`, `CreateSketchOn`), все
|
||||
примитивы (`AddLine/Circle/Rectangle/Axis` + 7 новых), `CloseSketch*`.
|
||||
- `PartModeler.Features.cs` — `ExtrudeAsync`/`RevolveAsync`/`CreateFillet`/`CreateChamfer`/
|
||||
`RebuildAsync`.
|
||||
|
||||
Операции по-прежнему берут `sketch.Entity` из общего `_sketches` — реестр не делится.
|
||||
Это targeted-улучшение «по ходу работы», контракт не меняется.
|
||||
|
||||
## Тестирование
|
||||
|
||||
**Unit** (`Category=Unit`, без COM): guard'ы `SketchGeometry` (`RequirePositive`,
|
||||
`RequireVertexCount`, `RequirePoints` — включая null и не-finite координаты) и маппинги
|
||||
`ArcDirection` (`counterClockwise→direction`), `PolygonDescribe` (`inscribed→describe`).
|
||||
(Координат вершин многоугольника вручную не считаем — метод нативный.)
|
||||
|
||||
**Integration** (`Category=Integration`, `KompasFixture`): для каждого профилирующего
|
||||
примитива — открыть эскиз → добавить примитив(ы), образующие замкнутый контур →
|
||||
`extrude_boss` → проверить правдоподобный объём:
|
||||
- `polygon`: правильный 6-угольник, `radius=10`, `extrude=5` → объём > 0, согласуется с
|
||||
площадью правильного шестиугольника.
|
||||
- `ellipse`: `semiMajor=10, semiMinor=5, extrude=4` → V ≈ π·a·b·h.
|
||||
- `arc_3points` / `arc`: дуга + замыкающий отрезок (хорда) → замкнутый сегмент → V > 0.
|
||||
- `polyline`: треугольник из 3 точек, `closed=true` → V > 0.
|
||||
- `spline`: замкнутый сплайн по 4+ точкам → V > 0.
|
||||
- `point`: вызов не падает, эскиз остаётся валидным (точка контур не образует — без
|
||||
проверки объёма).
|
||||
|
||||
## Влияние на документацию
|
||||
|
||||
После реализации обновить (через навык `docs-delegate`): счётчики инструментов/тестов и
|
||||
перечни в `CLAUDE.md`, `README.md`, `docs/ARCHITECTURE.md`; пометить пакет A выполненным в
|
||||
анализе пробелов. Память `kompas-step-and-assembly-api` не затрагивается.
|
||||
@@ -0,0 +1,206 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants3D;
|
||||
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
|
||||
/// <summary>PartModeler: формообразующие операции (выдавливание, вращение, скругление, фаска).</summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed partial class PartModeler
|
||||
{
|
||||
/// <summary>
|
||||
/// Выдавить эскиз. <paramref name="cut"/> — вырезание вместо добавления материала.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
/// <param name="throughAll">Сквозное выдавливание/вырез через всю деталь в обе стороны
|
||||
/// (для отверстий — гарантированно пробивает материал; <paramref name="depth"/> игнорируется).</param>
|
||||
public Task<int> ExtrudeAsync(
|
||||
int sketchId, double depth, bool cut = false, bool forward = true,
|
||||
double draftAngle = 0, bool throughAll = false, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (!throughAll && depth <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть > 0 (или используйте throughAll).");
|
||||
var sketch = RequireSketch(sketchId);
|
||||
CloseSketchCore(sketch); // эскиз должен быть закрыт перед операцией
|
||||
|
||||
var part = GetTopPart();
|
||||
var type = cut ? Obj3dType.o3d_cutExtrusion : Obj3dType.o3d_bossExtrusion;
|
||||
var entity = part.NewEntity((short)type) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity операции выдавливания вернул null.");
|
||||
|
||||
// Конкретные типы определений (без dynamic): ошибки связывания — на этапе компиляции.
|
||||
if (cut)
|
||||
{
|
||||
var def = entity.GetDefinition() as ksCutExtrusionDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() выреза вернул не ksCutExtrusionDefinition.");
|
||||
if (throughAll)
|
||||
{
|
||||
def.directionType = (short)Direction_Type.dtBoth;
|
||||
def.SetSideParam(true, (short)End_Type.etThroughAll, depth, draftAngle, false);
|
||||
def.SetSideParam(false, (short)End_Type.etThroughAll, depth, draftAngle, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
def.directionType = (short)Direction_Type.dtNormal;
|
||||
def.SetSideParam(forward, (short)End_Type.etBlind, depth, draftAngle, false);
|
||||
}
|
||||
def.SetSketch(sketch.Entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (throughAll)
|
||||
throw new ArgumentException("throughAll поддерживается только для выреза (cut).", nameof(throughAll));
|
||||
var def = entity.GetDefinition() as ksBossExtrusionDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() выдавливания вернул не ksBossExtrusionDefinition.");
|
||||
def.directionType = (short)Direction_Type.dtNormal;
|
||||
def.SetSideParam(forward, (short)End_Type.etBlind, depth, draftAngle, false);
|
||||
def.SetSketch(sketch.Entity);
|
||||
}
|
||||
|
||||
if (!entity.Create())
|
||||
throw new InvalidOperationException("Create() операции выдавливания вернул FALSE.");
|
||||
|
||||
var id = _nextId++;
|
||||
_features[id] = entity;
|
||||
return id;
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Вращать эскиз вокруг его осевой линии (добавьте её через AddAxis). <paramref name="cut"/> —
|
||||
/// вырезание вместо добавления материала. Угол <paramref name="angle"/> — в градусах.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> RevolveAsync(
|
||||
int sketchId, double angle = 360, bool cut = false, bool forward = true,
|
||||
CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (angle is <= 0 or > 360)
|
||||
throw new ArgumentOutOfRangeException(nameof(angle), "Угол вращения должен быть в (0; 360].");
|
||||
var sketch = RequireSketch(sketchId);
|
||||
CloseSketchCore(sketch); // эскиз должен быть закрыт перед операцией
|
||||
|
||||
var part = GetTopPart();
|
||||
var type = cut ? Obj3dType.o3d_cutRotated : Obj3dType.o3d_bossRotated;
|
||||
var entity = part.NewEntity((short)type) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity операции вращения вернул null.");
|
||||
|
||||
if (cut)
|
||||
{
|
||||
var def = entity.GetDefinition() as ksCutRotatedDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() выреза вращением вернул не ksCutRotatedDefinition.");
|
||||
def.directionType = (short)Direction_Type.dtNormal;
|
||||
def.SetThinParam(false, 0, 0, 0);
|
||||
def.SetSideParam(forward, angle);
|
||||
def.SetSketch(sketch.Entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
var def = entity.GetDefinition() as ksBossRotatedDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() вращения вернул не ksBossRotatedDefinition.");
|
||||
def.directionType = (short)Direction_Type.dtNormal;
|
||||
def.toroidShapeType = false; // сфероид (сплошное тело), не тороид
|
||||
def.SetThinParam(false, 0, 0, 0);
|
||||
def.SetSideParam(forward, angle);
|
||||
def.SetSketch(sketch.Entity);
|
||||
}
|
||||
|
||||
if (!entity.Create())
|
||||
throw new InvalidOperationException("Create() операции вращения вернул FALSE.");
|
||||
|
||||
var id = _nextId++;
|
||||
_features[id] = entity;
|
||||
return id;
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Скруглить ребро детали, найденное по точке (мировые координаты, мм), радиусом <paramref name="radius"/>.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> FilletEdgeAsync(double x, double y, double z, double radius, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
return CreateFillet(part, SelectEdgeByPoint(part, x, y, z), radius);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Скруглить ребро детали по индексу из list_edges радиусом <paramref name="radius"/>.</summary>
|
||||
public Task<int> FilletEdgeIndexAsync(int edgeIndex, double radius, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
return CreateFillet(part, SelectEdgeByIndex(part, edgeIndex), radius);
|
||||
}, ct);
|
||||
|
||||
private int CreateFillet(ksPart part, ksEntity edge, double radius)
|
||||
{
|
||||
if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус скругления должен быть > 0.");
|
||||
|
||||
var entity = part.NewEntity((short)Obj3dType.o3d_fillet) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity(o3d_fillet) вернул null.");
|
||||
var def = entity.GetDefinition() as ksFilletDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() скругления вернул не ksFilletDefinition.");
|
||||
def.radius = radius;
|
||||
def.tangent = false;
|
||||
var arr = def.array() as ksEntityCollection
|
||||
?? throw new InvalidOperationException("array() скругления вернул не ksEntityCollection.");
|
||||
arr.Add(edge);
|
||||
|
||||
if (!entity.Create())
|
||||
throw new InvalidOperationException("Create() скругления вернул FALSE (радиус слишком велик для ребра?).");
|
||||
|
||||
var id = _nextId++;
|
||||
_features[id] = entity;
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Снять фаску с ребра детали, найденного по точке (мировые координаты, мм), катетом <paramref name="distance"/>.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> ChamferEdgeAsync(double x, double y, double z, double distance, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
return CreateChamfer(part, SelectEdgeByPoint(part, x, y, z), distance);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Снять фаску с ребра детали по индексу из list_edges катетом <paramref name="distance"/>.</summary>
|
||||
public Task<int> ChamferEdgeIndexAsync(int edgeIndex, double distance, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
return CreateChamfer(part, SelectEdgeByIndex(part, edgeIndex), distance);
|
||||
}, ct);
|
||||
|
||||
private int CreateChamfer(ksPart part, ksEntity edge, double distance)
|
||||
{
|
||||
if (distance <= 0) throw new ArgumentOutOfRangeException(nameof(distance), "Катет фаски должен быть > 0.");
|
||||
|
||||
var entity = part.NewEntity((short)Obj3dType.o3d_chamfer) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity(o3d_chamfer) вернул null.");
|
||||
var def = entity.GetDefinition() as ksChamferDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() фаски вернул не ksChamferDefinition.");
|
||||
def.tangent = false;
|
||||
def.SetChamferParam(true, distance, distance);
|
||||
var arr = def.array() as ksEntityCollection
|
||||
?? throw new InvalidOperationException("array() фаски вернул не ksEntityCollection.");
|
||||
arr.Add(edge);
|
||||
|
||||
if (!entity.Create())
|
||||
throw new InvalidOperationException("Create() фаски вернул FALSE (катет слишком велик для ребра?).");
|
||||
|
||||
var id = _nextId++;
|
||||
_features[id] = entity;
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>Перестроить документ.</summary>
|
||||
public Task RebuildAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var doc3d = ActiveDoc3D();
|
||||
doc3d.RebuildDocument();
|
||||
}, ct);
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants;
|
||||
using Kompas6Constants3D;
|
||||
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
|
||||
/// <summary>PartModeler: создание эскизов и добавление 2D-примитивов.</summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed partial class PartModeler
|
||||
{
|
||||
/// <summary>Создать эскиз на базовой плоскости и открыть его на редактирование. Возвращает id эскиза.</summary>
|
||||
public Task<int> OpenSketchAsync(BasePlane plane, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity
|
||||
?? throw new InvalidOperationException("Не удалось получить базовую плоскость.");
|
||||
return CreateSketchOn(part, planeEntity);
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Создать эскиз на плоской грани детали, найденной по точке <paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>
|
||||
/// (мировые координаты модели, мм), и открыть его на редактирование. Возвращает id эскиза.
|
||||
/// Это ключ к итеративному циклу «построил → выбрал грань → новый эскиз на ней».
|
||||
/// </summary>
|
||||
public Task<int> OpenSketchOnFaceAsync(double x, double y, double z, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection
|
||||
?? throw new InvalidOperationException("Не удалось получить коллекцию граней.");
|
||||
// Оставляем в коллекции только грани, проходящие через указанную точку.
|
||||
faces.SelectByPoint(x, y, z);
|
||||
if (faces.GetCount() == 0)
|
||||
throw new InvalidOperationException(
|
||||
$"В точке ({x}, {y}, {z}) не найдено грани. Сделайте снимок и сверьте координаты.");
|
||||
|
||||
var face = faces.GetByIndex(0) as ksEntity
|
||||
?? throw new InvalidOperationException("Грань из коллекции не приводится к ksEntity.");
|
||||
return CreateSketchOn(part, face);
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Создать эскиз на грани детали, заданной индексом из list_faces, и открыть на редактирование.
|
||||
/// Индекс стабилен, пока геометрия не меняется. Возвращает id эскиза.
|
||||
/// </summary>
|
||||
public Task<int> OpenSketchOnFaceIndexAsync(int faceIndex, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
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.");
|
||||
|
||||
var face = faces.GetByIndex(faceIndex) as ksEntity
|
||||
?? throw new InvalidOperationException("Грань по индексу не приводится к ksEntity.");
|
||||
return CreateSketchOn(part, face);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Общее ядро: создать эскиз на заданной плоскости/грани и открыть на редактирование.</summary>
|
||||
private int CreateSketchOn(ksPart part, ksEntity planeEntity)
|
||||
{
|
||||
var entity = part.NewEntity((short)Obj3dType.o3d_sketch) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity(o3d_sketch) вернул null.");
|
||||
var def = entity.GetDefinition() as ksSketchDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() эскиза вернул не ksSketchDefinition.");
|
||||
|
||||
def.SetPlane(planeEntity);
|
||||
entity.Create();
|
||||
|
||||
var editor = def.BeginEdit() as ksDocument2D
|
||||
?? throw new InvalidOperationException("BeginEdit() не вернул ksDocument2D.");
|
||||
|
||||
var id = _nextId++;
|
||||
_sketches[id] = new SketchEntry { Entity = entity, Definition = def, Editor = editor };
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>Добавить отрезок в открытый эскиз.</summary>
|
||||
public Task AddLineAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksLineSeg(x1, y1, x2, y2, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksLineSeg вернул 0 (отрезок не создан).");
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить окружность в открытый эскиз.</summary>
|
||||
public Task AddCircleAsync(int sketchId, double centerX, double centerY, double radius, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус должен быть > 0.");
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksCircle(centerX, centerY, radius, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksCircle вернул 0 (окружность не создана).");
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить прямоугольник (4 отрезка) по двум углам.</summary>
|
||||
public Task AddRectangleAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
void Seg(double ax, double ay, double bx, double by)
|
||||
{
|
||||
if (editor.ksLineSeg(ax, ay, bx, by, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksLineSeg вернул 0 (сторона прямоугольника не создана).");
|
||||
}
|
||||
Seg(x1, y1, x2, y1);
|
||||
Seg(x2, y1, x2, y2);
|
||||
Seg(x2, y2, x1, y2);
|
||||
Seg(x1, y2, x1, y1);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить осевую линию (стиль «осевая») в эскиз — ось для операции вращения.</summary>
|
||||
public Task AddAxisAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksLineSeg(x1, y1, x2, y2, AxisLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksLineSeg (ось) вернул 0 (ось не создана).");
|
||||
}, ct);
|
||||
|
||||
/// <summary>Завершить редактирование эскиза.</summary>
|
||||
public Task CloseSketchAsync(int sketchId, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => CloseSketchCore(RequireSketch(sketchId)), ct);
|
||||
|
||||
// ---- Новые примитивы (пакет A) -----------------------------------------
|
||||
|
||||
private const short SplineOrder = 4; // порядок NURBS (степень+1); 4 = кубический сплайн
|
||||
private const int PointStyle = 0; // системный стиль точки (см. SDK pstyles)
|
||||
|
||||
/// <summary>Добавить дугу по трём точкам (начало, точка на дуге, конец) в открытый эскиз.</summary>
|
||||
public Task AddArc3PointsAsync(int sketchId, double x1, double y1, double x2, double y2, double x3, double y3, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksArcBy3Points(x1, y1, x2, y2, x3, y3, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksArcBy3Points вернул 0 (дуга не создана — три точки коллинеарны?).");
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить дугу по центру, радиусу и углам (градусы) в открытый эскиз.</summary>
|
||||
public Task AddArcByAngleAsync(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequirePositive(radius, nameof(radius));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
var dir = SketchGeometry.ArcDirection(counterClockwise);
|
||||
if (editor.ksArcByAngle(centerX, centerY, radius, startAngle, endAngle, dir, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksArcByAngle вернул 0 (дуга не создана).");
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить эллипс: центр, полуоси A/B (мм), угол наклона большой оси к X (градусы).</summary>
|
||||
public Task AddEllipseAsync(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequirePositive(semiMajor, nameof(semiMajor));
|
||||
SketchGeometry.RequirePositive(semiMinor, nameof(semiMinor));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
ksEllipseParam? p = null;
|
||||
try
|
||||
{
|
||||
p = NewParam<ksEllipseParam>(StructType2DEnum.ko_EllipseParam);
|
||||
// ВНИМАНИЕ: interop генерирует свойства A/B с ЗАГЛАВНОЙ (не a/b как в SDK-доках). Регистр в C# важен.
|
||||
p.xc = centerX; p.yc = centerY;
|
||||
p.A = semiMajor; p.B = semiMinor;
|
||||
p.angle = angle; p.style = MainLineStyle;
|
||||
if (editor.ksEllipse(p) == 0)
|
||||
throw new InvalidOperationException("ksEllipse вернул 0 (эллипс не создан).");
|
||||
}
|
||||
finally { ReleaseCom(p); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить ломаную (цепочку отрезков) по списку точек. closed замыкает последнюю с первой.</summary>
|
||||
public Task AddPolylineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
// closed-ломаная из 2 точек не образует площадь — требуем минимум 3.
|
||||
SketchGeometry.RequirePoints(points, closed ? 3 : 2, nameof(points));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
for (int i = 0; i + 1 < points.Count; i++)
|
||||
Seg(editor, points[i], points[i + 1]);
|
||||
if (closed)
|
||||
Seg(editor, points[^1], points[0]);
|
||||
|
||||
static void Seg(ksDocument2D ed, (double x, double y) a, (double x, double y) b)
|
||||
{
|
||||
if (ed.ksLineSeg(a.x, a.y, b.x, b.y, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksLineSeg вернул 0 (сегмент ломаной не создан).");
|
||||
}
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить правильный многоугольник: центр, vertexCount вершин, radius (мм),
|
||||
/// inscribed=true — вершины на окружности (вписанный), false — стороны касаются (описанный),
|
||||
/// angle — поворот первой вершины (градусы).</summary>
|
||||
public Task AddPolygonAsync(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequireVertexCount(vertexCount);
|
||||
SketchGeometry.RequirePositive(radius, nameof(radius));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
ksRegularPolygonParam? p = null;
|
||||
try
|
||||
{
|
||||
p = NewParam<ksRegularPolygonParam>(StructType2DEnum.ko_RegularPolygonParam);
|
||||
p.count = vertexCount; p.xc = centerX; p.yc = centerY;
|
||||
p.radius = radius; p.ang = angle;
|
||||
p.describe = SketchGeometry.PolygonDescribe(inscribed);
|
||||
p.style = MainLineStyle;
|
||||
if (editor.ksRegularPolygon(p, 0) == 0)
|
||||
throw new InvalidOperationException("ksRegularPolygon вернул 0 (многоугольник не создан).");
|
||||
}
|
||||
finally { ReleaseCom(p); }
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить сплайн (кубический NURBS, порядок 4) через список точек. closed замыкает кривую.</summary>
|
||||
public Task AddSplineAsync(int sketchId, IReadOnlyList<(double x, double y)> points, bool closed = false, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
SketchGeometry.RequirePoints(points, 2, nameof(points));
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksNurbs(SplineOrder, closed, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksNurbs вернул 0 (сплайн не открыт).");
|
||||
// ksNurbs открыл составной объект — ksEndObj ОБЯЗАТЕЛЕН в любом исходе, иначе редактор «застрянет».
|
||||
var completed = false;
|
||||
try
|
||||
{
|
||||
foreach (var (x, y) in points)
|
||||
{
|
||||
ksNurbsPointParam? np = null;
|
||||
try
|
||||
{
|
||||
np = NewParam<ksNurbsPointParam>(StructType2DEnum.ko_NurbsPointParam);
|
||||
np.x = x; np.y = y; np.weight = 1.0;
|
||||
if (editor.ksNurbsPoint(np) == 0)
|
||||
throw new InvalidOperationException("ksNurbsPoint вернул 0 (узел сплайна не добавлен).");
|
||||
}
|
||||
finally { ReleaseCom(np); }
|
||||
}
|
||||
completed = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Закрываем составной объект всегда; ошибку завершения сообщаем только на успешном пути,
|
||||
// чтобы не подменить исходное исключение из цикла.
|
||||
var end = editor.ksEndObj();
|
||||
if (completed && end == 0)
|
||||
throw new InvalidOperationException("ksEndObj вернул 0 (сплайн не завершён).");
|
||||
}
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить точку в открытый эскиз (опорная точка для построений).</summary>
|
||||
public Task AddPointAsync(int sketchId, double x, double y, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksPoint(x, y, PointStyle) == 0)
|
||||
throw new InvalidOperationException("ksPoint вернул 0 (точка не создана).");
|
||||
}, ct);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Interop;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6API5;
|
||||
using Kompas6Constants;
|
||||
using Kompas6Constants3D;
|
||||
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
@@ -10,9 +11,11 @@ namespace Kompas.Mcp.Core.Modeling;
|
||||
/// Построение 3D-детали через API5 (<c>ksPart</c>): эскизы и формообразующие операции.
|
||||
/// Эскизы и операции регистрируются под целочисленными id, чтобы инструменты MCP могли
|
||||
/// ссылаться на них в цикле «эскиз → операция → эскиз».
|
||||
/// <para>Класс разбит на partial-файлы: ядро (этот файл), <c>PartModeler.Sketch.cs</c>
|
||||
/// (эскизы и 2D-примитивы), <c>PartModeler.Features.cs</c> (формообразующие операции).</para>
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class PartModeler : IDisposable
|
||||
public sealed partial class PartModeler : IDisposable
|
||||
{
|
||||
private const int MainLineStyle = 1; // системный стиль «Основная»
|
||||
private const int AxisLineStyle = 3; // системный стиль «Осевая» (ось вращения)
|
||||
@@ -37,324 +40,6 @@ public sealed class PartModeler : IDisposable
|
||||
public ksDocument2D? Editor { get; set; } // не null, пока эскиз открыт на редактирование
|
||||
}
|
||||
|
||||
// ---- Эскизы ------------------------------------------------------------
|
||||
|
||||
/// <summary>Создать эскиз на базовой плоскости и открыть его на редактирование. Возвращает id эскиза.</summary>
|
||||
public Task<int> OpenSketchAsync(BasePlane plane, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
var planeEntity = part.GetDefaultEntity((short)BasePlanes.ToObj3dType(plane)) as ksEntity
|
||||
?? throw new InvalidOperationException("Не удалось получить базовую плоскость.");
|
||||
return CreateSketchOn(part, planeEntity);
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Создать эскиз на плоской грани детали, найденной по точке <paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>
|
||||
/// (мировые координаты модели, мм), и открыть его на редактирование. Возвращает id эскиза.
|
||||
/// Это ключ к итеративному циклу «построил → выбрал грань → новый эскиз на ней».
|
||||
/// </summary>
|
||||
public Task<int> OpenSketchOnFaceAsync(double x, double y, double z, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection
|
||||
?? throw new InvalidOperationException("Не удалось получить коллекцию граней.");
|
||||
// Оставляем в коллекции только грани, проходящие через указанную точку.
|
||||
faces.SelectByPoint(x, y, z);
|
||||
if (faces.GetCount() == 0)
|
||||
throw new InvalidOperationException(
|
||||
$"В точке ({x}, {y}, {z}) не найдено грани. Сделайте снимок и сверьте координаты.");
|
||||
|
||||
var face = faces.GetByIndex(0) as ksEntity
|
||||
?? throw new InvalidOperationException("Грань из коллекции не приводится к ksEntity.");
|
||||
return CreateSketchOn(part, face);
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Создать эскиз на грани детали, заданной индексом из list_faces, и открыть на редактирование.
|
||||
/// Индекс стабилен, пока геометрия не меняется. Возвращает id эскиза.
|
||||
/// </summary>
|
||||
public Task<int> OpenSketchOnFaceIndexAsync(int faceIndex, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
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.");
|
||||
|
||||
var face = faces.GetByIndex(faceIndex) as ksEntity
|
||||
?? throw new InvalidOperationException("Грань по индексу не приводится к ksEntity.");
|
||||
return CreateSketchOn(part, face);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Общее ядро: создать эскиз на заданной плоскости/грани и открыть на редактирование.</summary>
|
||||
private int CreateSketchOn(ksPart part, ksEntity planeEntity)
|
||||
{
|
||||
var entity = part.NewEntity((short)Obj3dType.o3d_sketch) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity(o3d_sketch) вернул null.");
|
||||
var def = entity.GetDefinition() as ksSketchDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() эскиза вернул не ksSketchDefinition.");
|
||||
|
||||
def.SetPlane(planeEntity);
|
||||
entity.Create();
|
||||
|
||||
var editor = def.BeginEdit() as ksDocument2D
|
||||
?? throw new InvalidOperationException("BeginEdit() не вернул ksDocument2D.");
|
||||
|
||||
var id = _nextId++;
|
||||
_sketches[id] = new SketchEntry { Entity = entity, Definition = def, Editor = editor };
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>Добавить отрезок в открытый эскиз.</summary>
|
||||
public Task AddLineAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksLineSeg(x1, y1, x2, y2, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksLineSeg вернул 0 (отрезок не создан).");
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить окружность в открытый эскиз.</summary>
|
||||
public Task AddCircleAsync(int sketchId, double centerX, double centerY, double radius, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус должен быть > 0.");
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksCircle(centerX, centerY, radius, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksCircle вернул 0 (окружность не создана).");
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить прямоугольник (4 отрезка) по двум углам.</summary>
|
||||
public Task AddRectangleAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
void Seg(double ax, double ay, double bx, double by)
|
||||
{
|
||||
if (editor.ksLineSeg(ax, ay, bx, by, MainLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksLineSeg вернул 0 (сторона прямоугольника не создана).");
|
||||
}
|
||||
Seg(x1, y1, x2, y1);
|
||||
Seg(x2, y1, x2, y2);
|
||||
Seg(x2, y2, x1, y2);
|
||||
Seg(x1, y2, x1, y1);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Добавить осевую линию (стиль «осевая») в эскиз — ось для операции вращения.</summary>
|
||||
public Task AddAxisAsync(int sketchId, double x1, double y1, double x2, double y2, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var editor = RequireOpenSketch(sketchId).Editor!;
|
||||
if (editor.ksLineSeg(x1, y1, x2, y2, AxisLineStyle) == 0)
|
||||
throw new InvalidOperationException("ksLineSeg (ось) вернул 0 (ось не создана).");
|
||||
}, ct);
|
||||
|
||||
/// <summary>Завершить редактирование эскиза.</summary>
|
||||
public Task CloseSketchAsync(int sketchId, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => CloseSketchCore(RequireSketch(sketchId)), ct);
|
||||
|
||||
// ---- Операции ----------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Выдавить эскиз. <paramref name="cut"/> — вырезание вместо добавления материала.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
/// <param name="throughAll">Сквозное выдавливание/вырез через всю деталь в обе стороны
|
||||
/// (для отверстий — гарантированно пробивает материал; <paramref name="depth"/> игнорируется).</param>
|
||||
public Task<int> ExtrudeAsync(
|
||||
int sketchId, double depth, bool cut = false, bool forward = true,
|
||||
double draftAngle = 0, bool throughAll = false, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (!throughAll && depth <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(depth), "Глубина должна быть > 0 (или используйте throughAll).");
|
||||
var sketch = RequireSketch(sketchId);
|
||||
CloseSketchCore(sketch); // эскиз должен быть закрыт перед операцией
|
||||
|
||||
var part = GetTopPart();
|
||||
var type = cut ? Obj3dType.o3d_cutExtrusion : Obj3dType.o3d_bossExtrusion;
|
||||
var entity = part.NewEntity((short)type) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity операции выдавливания вернул null.");
|
||||
|
||||
// Конкретные типы определений (без dynamic): ошибки связывания — на этапе компиляции.
|
||||
if (cut)
|
||||
{
|
||||
var def = entity.GetDefinition() as ksCutExtrusionDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() выреза вернул не ksCutExtrusionDefinition.");
|
||||
if (throughAll)
|
||||
{
|
||||
def.directionType = (short)Direction_Type.dtBoth;
|
||||
def.SetSideParam(true, (short)End_Type.etThroughAll, depth, draftAngle, false);
|
||||
def.SetSideParam(false, (short)End_Type.etThroughAll, depth, draftAngle, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
def.directionType = (short)Direction_Type.dtNormal;
|
||||
def.SetSideParam(forward, (short)End_Type.etBlind, depth, draftAngle, false);
|
||||
}
|
||||
def.SetSketch(sketch.Entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (throughAll)
|
||||
throw new ArgumentException("throughAll поддерживается только для выреза (cut).", nameof(throughAll));
|
||||
var def = entity.GetDefinition() as ksBossExtrusionDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() выдавливания вернул не ksBossExtrusionDefinition.");
|
||||
def.directionType = (short)Direction_Type.dtNormal;
|
||||
def.SetSideParam(forward, (short)End_Type.etBlind, depth, draftAngle, false);
|
||||
def.SetSketch(sketch.Entity);
|
||||
}
|
||||
|
||||
if (!entity.Create())
|
||||
throw new InvalidOperationException("Create() операции выдавливания вернул FALSE.");
|
||||
|
||||
var id = _nextId++;
|
||||
_features[id] = entity;
|
||||
return id;
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Вращать эскиз вокруг его осевой линии (добавьте её через AddAxis). <paramref name="cut"/> —
|
||||
/// вырезание вместо добавления материала. Угол <paramref name="angle"/> — в градусах.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> RevolveAsync(
|
||||
int sketchId, double angle = 360, bool cut = false, bool forward = true,
|
||||
CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (angle is <= 0 or > 360)
|
||||
throw new ArgumentOutOfRangeException(nameof(angle), "Угол вращения должен быть в (0; 360].");
|
||||
var sketch = RequireSketch(sketchId);
|
||||
CloseSketchCore(sketch); // эскиз должен быть закрыт перед операцией
|
||||
|
||||
var part = GetTopPart();
|
||||
var type = cut ? Obj3dType.o3d_cutRotated : Obj3dType.o3d_bossRotated;
|
||||
var entity = part.NewEntity((short)type) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity операции вращения вернул null.");
|
||||
|
||||
if (cut)
|
||||
{
|
||||
var def = entity.GetDefinition() as ksCutRotatedDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() выреза вращением вернул не ksCutRotatedDefinition.");
|
||||
def.directionType = (short)Direction_Type.dtNormal;
|
||||
def.SetThinParam(false, 0, 0, 0);
|
||||
def.SetSideParam(forward, angle);
|
||||
def.SetSketch(sketch.Entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
var def = entity.GetDefinition() as ksBossRotatedDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() вращения вернул не ksBossRotatedDefinition.");
|
||||
def.directionType = (short)Direction_Type.dtNormal;
|
||||
def.toroidShapeType = false; // сфероид (сплошное тело), не тороид
|
||||
def.SetThinParam(false, 0, 0, 0);
|
||||
def.SetSideParam(forward, angle);
|
||||
def.SetSketch(sketch.Entity);
|
||||
}
|
||||
|
||||
if (!entity.Create())
|
||||
throw new InvalidOperationException("Create() операции вращения вернул FALSE.");
|
||||
|
||||
var id = _nextId++;
|
||||
_features[id] = entity;
|
||||
return id;
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Скруглить ребро детали, найденное по точке (мировые координаты, мм), радиусом <paramref name="radius"/>.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> FilletEdgeAsync(double x, double y, double z, double radius, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
return CreateFillet(part, SelectEdgeByPoint(part, x, y, z), radius);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Скруглить ребро детали по индексу из list_edges радиусом <paramref name="radius"/>.</summary>
|
||||
public Task<int> FilletEdgeIndexAsync(int edgeIndex, double radius, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
return CreateFillet(part, SelectEdgeByIndex(part, edgeIndex), radius);
|
||||
}, ct);
|
||||
|
||||
private int CreateFillet(ksPart part, ksEntity edge, double radius)
|
||||
{
|
||||
if (radius <= 0) throw new ArgumentOutOfRangeException(nameof(radius), "Радиус скругления должен быть > 0.");
|
||||
|
||||
var entity = part.NewEntity((short)Obj3dType.o3d_fillet) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity(o3d_fillet) вернул null.");
|
||||
var def = entity.GetDefinition() as ksFilletDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() скругления вернул не ksFilletDefinition.");
|
||||
def.radius = radius;
|
||||
def.tangent = false;
|
||||
var arr = def.array() as ksEntityCollection
|
||||
?? throw new InvalidOperationException("array() скругления вернул не ksEntityCollection.");
|
||||
arr.Add(edge);
|
||||
|
||||
if (!entity.Create())
|
||||
throw new InvalidOperationException("Create() скругления вернул FALSE (радиус слишком велик для ребра?).");
|
||||
|
||||
var id = _nextId++;
|
||||
_features[id] = entity;
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Снять фаску с ребра детали, найденного по точке (мировые координаты, мм), катетом <paramref name="distance"/>.
|
||||
/// Возвращает id операции.
|
||||
/// </summary>
|
||||
public Task<int> ChamferEdgeAsync(double x, double y, double z, double distance, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
return CreateChamfer(part, SelectEdgeByPoint(part, x, y, z), distance);
|
||||
}, ct);
|
||||
|
||||
/// <summary>Снять фаску с ребра детали по индексу из list_edges катетом <paramref name="distance"/>.</summary>
|
||||
public Task<int> ChamferEdgeIndexAsync(int edgeIndex, double distance, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var part = GetTopPart();
|
||||
return CreateChamfer(part, SelectEdgeByIndex(part, edgeIndex), distance);
|
||||
}, ct);
|
||||
|
||||
private int CreateChamfer(ksPart part, ksEntity edge, double distance)
|
||||
{
|
||||
if (distance <= 0) throw new ArgumentOutOfRangeException(nameof(distance), "Катет фаски должен быть > 0.");
|
||||
|
||||
var entity = part.NewEntity((short)Obj3dType.o3d_chamfer) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity(o3d_chamfer) вернул null.");
|
||||
var def = entity.GetDefinition() as ksChamferDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() фаски вернул не ksChamferDefinition.");
|
||||
def.tangent = false;
|
||||
def.SetChamferParam(true, distance, distance);
|
||||
var arr = def.array() as ksEntityCollection
|
||||
?? throw new InvalidOperationException("array() фаски вернул не ksEntityCollection.");
|
||||
arr.Add(edge);
|
||||
|
||||
if (!entity.Create())
|
||||
throw new InvalidOperationException("Create() фаски вернул FALSE (катет слишком велик для ребра?).");
|
||||
|
||||
var id = _nextId++;
|
||||
_features[id] = entity;
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>Перестроить документ.</summary>
|
||||
public Task RebuildAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
var doc3d = ActiveDoc3D();
|
||||
doc3d.RebuildDocument();
|
||||
}, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Сбросить реестр эскизов/операций и освободить их COM-объекты. Вызывать при смене
|
||||
/// активного документа (создание/открытие/закрытие) — id больше не действительны.
|
||||
@@ -364,6 +49,16 @@ public sealed class PartModeler : IDisposable
|
||||
|
||||
// ---- Внутреннее --------------------------------------------------------
|
||||
|
||||
/// <summary>Создать параметрическую 2D-структуру через KompasObject (API5) и привести к интерфейсу.
|
||||
/// При неудачном приведении освобождает сырой RCW, чтобы не утекал COM-объект.</summary>
|
||||
private T NewParam<T>(StructType2DEnum kind) where T : class
|
||||
{
|
||||
var raw = _session.Kompas.GetParamStruct((short)kind);
|
||||
if (raw is T typed) return typed;
|
||||
ReleaseCom(raw);
|
||||
throw new InvalidOperationException($"GetParamStruct({kind}) вернул не {typeof(T).Name}.");
|
||||
}
|
||||
|
||||
private ksDocument3D ActiveDoc3D()
|
||||
=> _session.Kompas.ActiveDocument3D() as ksDocument3D
|
||||
?? throw new InvalidOperationException("Нет активного 3D-документа.");
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace Kompas.Mcp.Core.Modeling;
|
||||
|
||||
/// <summary>Чистые помощники геометрии эскиза: маппинги контракта в параметры COM и валидация.</summary>
|
||||
public static class SketchGeometry
|
||||
{
|
||||
/// <summary>Направление дуги для ksArcByAngle: против часовой → 1, по часовой → -1.</summary>
|
||||
public static short ArcDirection(bool counterClockwise) => (short)(counterClockwise ? 1 : -1);
|
||||
|
||||
/// <summary>describe для ksRegularPolygon: вписанный (вершины на окружности) → false; описанный → true.</summary>
|
||||
public static bool PolygonDescribe(bool inscribed) => !inscribed;
|
||||
|
||||
/// <summary>Требовать строго положительное значение (радиус, полуось).</summary>
|
||||
public static void RequirePositive(double value, string paramName)
|
||||
{
|
||||
if (!(value > 0))
|
||||
throw new ArgumentOutOfRangeException(paramName, value, "Значение должно быть > 0.");
|
||||
}
|
||||
|
||||
/// <summary>Требовать >= 3 вершин для правильного многоугольника.</summary>
|
||||
public static void RequireVertexCount(int count)
|
||||
{
|
||||
if (count < 3)
|
||||
throw new ArgumentOutOfRangeException(nameof(count), count, "Число вершин должно быть >= 3.");
|
||||
}
|
||||
|
||||
/// <summary>Валидировать список точек: не null, минимум <paramref name="min"/>, все координаты конечны.</summary>
|
||||
public static void RequirePoints(IReadOnlyList<(double x, double y)> points, int min, string paramName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(points, paramName);
|
||||
if (points.Count < min)
|
||||
throw new ArgumentException($"Нужно минимум {min} точек, передано {points.Count}.", paramName);
|
||||
for (int i = 0; i < points.Count; i++)
|
||||
if (!double.IsFinite(points[i].x) || !double.IsFinite(points[i].y))
|
||||
throw new ArgumentException($"Точка [{i}] имеет неконечную координату.", paramName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Kompas.Mcp.Host.Tools;
|
||||
|
||||
/// <summary>Точка эскиза в его плоскости (мм). Элемент списка для ломаной/сплайна.
|
||||
/// JSON-имена зафиксированы lowercase (x/y), чтобы схема инструмента совпадала с контрактом.</summary>
|
||||
public sealed record SketchPoint(
|
||||
[property: JsonPropertyName("x")][property: Description("Координата X в плоскости эскиза, мм")] double X,
|
||||
[property: JsonPropertyName("y")][property: Description("Координата Y в плоскости эскиза, мм")] double Y);
|
||||
@@ -80,4 +80,73 @@ public sealed class SketchTools(KompasSession session, PartModeler modeler)
|
||||
await modeler.CloseSketchAsync(sketchId);
|
||||
return "Эскиз закрыт.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_arc_3points")]
|
||||
[Description("Добавить дугу по трём точкам (начало, промежуточная точка на дуге, конец) в открытый эскиз. Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddArc3Points(int sketchId, double x1, double y1, double x2, double y2, double x3, double y3)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddArc3PointsAsync(sketchId, x1, y1, x2, y2, x3, y3);
|
||||
return "Дуга по 3 точкам добавлена.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_arc")]
|
||||
[Description("Добавить дугу по центру, радиусу и углам (в градусах, от оси X) в открытый эскиз. counterClockwise=true — против часовой стрелки, false — по часовой. Координаты центра — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddArc(int sketchId, double centerX, double centerY, double radius, double startAngle, double endAngle, bool counterClockwise = true)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddArcByAngleAsync(sketchId, centerX, centerY, radius, startAngle, endAngle, counterClockwise);
|
||||
return "Дуга добавлена.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_ellipse")]
|
||||
[Description("Добавить эллипс в открытый эскиз: центр, большая полуось semiMajor, малая полуось semiMinor (мм), угол наклона большой оси к X (градусы, по умолчанию 0).")]
|
||||
public async Task<string> AddEllipse(int sketchId, double centerX, double centerY, double semiMajor, double semiMinor, double angle = 0)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddEllipseAsync(sketchId, centerX, centerY, semiMajor, semiMinor, angle);
|
||||
return "Эллипс добавлен.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_polyline")]
|
||||
[Description("Добавить ломаную — цепочку прямых отрезков по списку точек — в открытый эскиз. closed=true замыкает последнюю точку с первой (тогда нужно минимум 3 точки). Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddPolyline(int sketchId, SketchPoint[] points, bool closed = false)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddPolylineAsync(sketchId, Map(points), closed);
|
||||
return $"Ломаная из {points.Length} точек добавлена{(closed ? " (замкнута)" : "")}.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_polygon")]
|
||||
[Description("Добавить правильный многоугольник в открытый эскиз: центр, vertexCount вершин (>=3), radius (мм). inscribed=true — вершины лежат на окружности радиуса (вписанный в окружность); false — стороны касаются окружности (описанный). angle — поворот первой вершины (градусы, по умолчанию 0).")]
|
||||
public async Task<string> AddPolygon(int sketchId, double centerX, double centerY, int vertexCount, double radius, bool inscribed = true, double angle = 0)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddPolygonAsync(sketchId, centerX, centerY, vertexCount, radius, inscribed, angle);
|
||||
return $"Правильный многоугольник ({vertexCount} вершин) добавлен.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_spline")]
|
||||
[Description("Добавить сплайн (кубический NURBS) через список точек в открытый эскиз. closed=true замыкает кривую. Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddSpline(int sketchId, SketchPoint[] points, bool closed = false)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddSplineAsync(sketchId, Map(points), closed);
|
||||
return $"Сплайн по {points.Length} точкам добавлен{(closed ? " (замкнут)" : "")}.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sketch_add_point")]
|
||||
[Description("Добавить точку в открытый эскиз (опорная точка для построений). Координаты — в плоскости эскиза, мм.")]
|
||||
public async Task<string> AddPoint(int sketchId, double x, double y)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
await modeler.AddPointAsync(sketchId, x, y);
|
||||
return "Точка добавлена.";
|
||||
}
|
||||
|
||||
private static IReadOnlyList<(double x, double y)> Map(SketchPoint[] points)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(points);
|
||||
return points.Select(p => (p.X, p.Y)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
using Kompas.Mcp.Core.Query;
|
||||
|
||||
namespace Kompas.Mcp.Tests.Integration;
|
||||
|
||||
/// <summary>Интеграция: новые примитивы эскиза (профиль → выдавливание → объём).</summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Collection(KompasCollection.Name)]
|
||||
public sealed class SketchPrimitivesTests
|
||||
{
|
||||
private readonly DocumentService _docs;
|
||||
private readonly PartModeler _modeler;
|
||||
private readonly QueryService _query;
|
||||
|
||||
public SketchPrimitivesTests(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 Arc3Points_semicircle_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Верхний полукруг R10: дуга (10,0)→(0,10)→(-10,0) + хорда обратно.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddArc3PointsAsync(s, 10, 0, 0, 10, -10, 0);
|
||||
await _modeler.AddLineAsync(s, -10, 0, 10, 0);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·10²/2·5 ≈ 785.4
|
||||
var expected = Math.PI * 100 / 2 * 5;
|
||||
Assert.InRange(v, expected * 0.95, expected * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ArcByAngle_quarter_sector_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Сектор 90° R10 в первом квадранте: дуга (10,0)→(0,10) + два радиуса к центру.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddArcByAngleAsync(s, 0, 0, 10, 0, 90, counterClockwise: true);
|
||||
await _modeler.AddLineAsync(s, 0, 10, 0, 0);
|
||||
await _modeler.AddLineAsync(s, 0, 0, 10, 0);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·10²/4·5 ≈ 392.7
|
||||
var expected = Math.PI * 100 / 4 * 5;
|
||||
Assert.InRange(v, expected * 0.95, expected * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ellipse_extrudes_to_expected_volume()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddEllipseAsync(s, 0, 0, semiMajor: 10, semiMinor: 5);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 4);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·10·5·4 ≈ 628.3
|
||||
var expected = Math.PI * 10 * 5 * 4;
|
||||
Assert.InRange(v, expected * 0.97, expected * 1.03);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Polyline_closed_triangle_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Прямоугольный треугольник (0,0)-(20,0)-(0,15), площадь 150.
|
||||
var pts = new (double x, double y)[] { (0, 0), (20, 0), (0, 15) };
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddPolylineAsync(s, pts, closed: true);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // 150·5 = 750
|
||||
Assert.InRange(v, 750 * 0.97, 750 * 1.03);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Polygon_hexagon_extrudes_to_expected_volume()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Правильный 6-угольник, вписанный в окружность R10: площадь = 0.5·6·R²·sin(60°) ≈ 259.8.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddPolygonAsync(s, 0, 0, vertexCount: 6, radius: 10, inscribed: true);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume;
|
||||
var area = 0.5 * 6 * 100 * Math.Sin(2 * Math.PI / 6);
|
||||
Assert.InRange(v, area * 5 * 0.95, area * 5 * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Spline_closed_loop_extrudes()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Замкнутый сплайн по 4 точкам вокруг начала координат → выпуклая «капля».
|
||||
var pts = new (double x, double y)[] { (10, 0), (0, 10), (-10, 0), (0, -10) };
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddSplineAsync(s, pts, closed: true);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
// Замкнутый кубический сплайн через 4 точки даёт гладкий вогнутый контур, вписанный
|
||||
// в габаритный квадрат 20×20 (площадь профиля ≈135 мм²). Проверяем, что тело построено
|
||||
// и объём положителен и не превышает габарит профиля (площадь ≤ 400 → V ≤ 2000).
|
||||
var v = (await _query.GetPartInfoAsync()).Volume;
|
||||
Assert.InRange(v, 50 * 5, 400 * 5);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Point_does_not_break_sketch()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Окружность R10 + точка в центре; выдавливание должно дать цилиндр без искажений.
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddCircleAsync(s, 0, 0, 10);
|
||||
await _modeler.AddPointAsync(s, 0, 0);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: 5);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
var v = (await _query.GetPartInfoAsync()).Volume; // π·100·5 ≈ 1570.8
|
||||
var expected = Math.PI * 100 * 5;
|
||||
Assert.InRange(v, expected * 0.97, expected * 1.03);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
|
||||
namespace Kompas.Mcp.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class SketchGeometryTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(true, 1)]
|
||||
[InlineData(false, -1)]
|
||||
public void ArcDirection_maps_orientation(bool ccw, int expected)
|
||||
=> Assert.Equal(expected, (int)SketchGeometry.ArcDirection(ccw));
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, false)] // вписанный → describe=false
|
||||
[InlineData(false, true)] // описанный → describe=true
|
||||
public void PolygonDescribe_inverts_inscribed(bool inscribed, bool expected)
|
||||
=> Assert.Equal(expected, SketchGeometry.PolygonDescribe(inscribed));
|
||||
|
||||
[Fact]
|
||||
public void RequirePositive_throws_on_zero_or_negative()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => SketchGeometry.RequirePositive(0, "radius"));
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => SketchGeometry.RequirePositive(-1, "radius"));
|
||||
Assert.Null(Record.Exception(() => SketchGeometry.RequirePositive(0.1, "radius")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequireVertexCount_throws_below_three()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => SketchGeometry.RequireVertexCount(2));
|
||||
Assert.Null(Record.Exception(() => SketchGeometry.RequireVertexCount(3)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RequirePoints_throws_on_null()
|
||||
=> Assert.Throws<ArgumentNullException>(
|
||||
() => SketchGeometry.RequirePoints(null!, 2, "points"));
|
||||
|
||||
[Fact]
|
||||
public void RequirePoints_throws_when_too_few()
|
||||
=> Assert.Throws<ArgumentException>(
|
||||
() => SketchGeometry.RequirePoints(new[] { (0.0, 0.0) }, 2, "points"));
|
||||
|
||||
[Fact]
|
||||
public void RequirePoints_throws_on_non_finite()
|
||||
=> Assert.Throws<ArgumentException>(
|
||||
() => SketchGeometry.RequirePoints(new[] { (0.0, 0.0), (double.NaN, 1.0) }, 2, "points"));
|
||||
|
||||
[Fact]
|
||||
public void RequirePoints_passes_on_valid()
|
||||
=> Assert.Null(Record.Exception(
|
||||
() => SketchGeometry.RequirePoints(new[] { (0.0, 0.0), (1.0, 1.0) }, 2, "points")));
|
||||
}
|
||||
Reference in New Issue
Block a user