Merge feat/assembly-add-component: вставка детали в сборку (класс СБОРКИ начат)
assembly_add_component — первый инструмент построения сборок (API7, AssemblyService): IParts7.AddFromFile → Placement.SetOrigin → RebuildModel. 71 инструмент, 138 тестов (83 unit + 55 integration). Спек + ревью Codex (спека и реализации) учтены. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe
|
||||
}
|
||||
```
|
||||
|
||||
## Инструменты (70 инструментов)
|
||||
## Инструменты (71 инструмент)
|
||||
|
||||
| Группа | Инструменты |
|
||||
|---|---|
|
||||
@@ -53,6 +53,7 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe
|
||||
| Query | `get_part_info`, `get_bounding_box`, `list_faces`, `list_edges`, `list_components` |
|
||||
| Variables | `create_variable`, `set_variable`, `delete_variable` |
|
||||
| Conversion | `import_step`, `export_step` |
|
||||
| Assembly | `assembly_add_component` |
|
||||
| Validation | `validate_part` |
|
||||
|
||||
### Типовой сценарий
|
||||
@@ -71,7 +72,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/ 119 тестов: unit + integration (integration требуют КОМПАС)
|
||||
tests/Kompas.Mcp.Tests/ 138 тестов: unit + integration (integration требуют КОМПАС)
|
||||
libs/kompas-interop/ вендорские interop-сборки КОМПАС (из SDK Samples/Common)
|
||||
docs/ архитектура, план, презентация, MD-база знаний SDK (Kompas3D_SDK/)
|
||||
usecases/ полигон обкатки подходов (в .gitignore); приёмы поднимаются в навык kompas-3d
|
||||
@@ -90,7 +91,7 @@ dotnet test --filter "Category=Integration" # только integration
|
||||
|
||||
- **STA-поток** (`KompasDispatcher`) владеет всеми COM-вызовами — КОМПАС однопоточный STA.
|
||||
- Подключение через API5 `KompasObject` (`KOMPAS.Application.5`) → API7 `IApplication` (`ksGetApplication7`).
|
||||
- 3D строится через API5 `ksPart` (эскизы, выдавливание, вращение, скругление/фаска); снимок — `ksDocument3D.SaveAsToRasterFormat`. Прямое редактирование импортированной B-rep (`move_face`) — через API7 `IPart7.FindObjectsByPoint` + `FaceMover`. Структурный осмотр (`describe_model` и др.) — через `ModelInspectionService` (дерево операций via `ksPart.GetFeature()`, тела via `BodyCollection()`, переменные via `GetFeature().VariableCollection`, измерения via `ksMeasurer`). Управление переменными (`create_variable`, `set_variable`, `delete_variable`) — через `VariableService` (API5).
|
||||
- 3D строится через API5 `ksPart` (эскизы, выдавливание, вращение, скругление/фаска); снимок — `ksDocument3D.SaveAsToRasterFormat`. Прямое редактирование импортированной B-rep (`move_face`) — через API7 `IPart7.FindObjectsByPoint` + `FaceMover`. Структурный осмотр (`describe_model` и др.) — через `ModelInspectionService` (дерево операций via `ksPart.GetFeature()`, тела via `BodyCollection()`, переменные via `GetFeature().VariableCollection`, измерения via `ksMeasurer`). Управление переменными (`create_variable`, `set_variable`, `delete_variable`) — через `VariableService` (API5). Вставка компонента в сборку (`assembly_add_component`) — через `AssemblyService` (API7, `IParts7.AddFromFile` + `IPlacement3D`).
|
||||
- STEP импорт/экспорт — `IKompasDocument3D1.ConvertFromAdditionFormat`/`ConvertToAdditionFormat` через встроенный конвертер.
|
||||
- MCP — официальный C# SDK `ModelContextProtocol`, транспорт stdio, логи в stderr.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Архитектура MCP-сервера КОМПАС-3D (предлагаемый вариант)
|
||||
|
||||
> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection+package-A «богатые эскизы»+package-B «shell+rib+sweep+loft»+package-C «массивы и зеркало»+package-D start «переменные»+package-E «offset plane»+`hole`+`hole_counterbore`+`hole_countersink`+`hole_conic`+`draft`). Актуализировано по коду.
|
||||
> Статус: **реализовано и работает** (v1+v2+STEP/assembly+direct-edit+inspection+package-A «богатые эскизы»+package-B «shell+rib+sweep+loft»+package-C «массивы и зеркало»+package-D start «переменные»+package-E «offset plane»+`hole`+`hole_counterbore`+`hole_countersink`+`hole_conic`+`draft`+`assembly_add_component`). Актуализировано по коду.
|
||||
> Сопутствующий контекст по COM API КОМПАС — в [`../CLAUDE.md`](../CLAUDE.md).
|
||||
> Имена интерфейсов и перечислений сверены со справкой SDK по MD-базе знаний `docs/Kompas3D_SDK/` (навык `kompas-sdk-research`).
|
||||
|
||||
@@ -112,8 +112,8 @@ 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) + интеграционные (требуют КОМПАС). 119 тестов (69 unit + 50 integration). |
|
||||
| **Kompas.Mcp.Core** | COM-слой. Менеджер соединения, менеджер документов, построитель эскизов и операций, `ConversionService` (STEP импорт/экспорт), `QueryService` (МЦХ, грани, рёбра, компоненты), `ModelInspectionService` (дерево операций, тела, переменные, drill-down, измерения), `AssemblyService` (вставка компонентов в сборку, namespace `Kompas.Mcp.Core.Assemblies`), снимок. Все паттерны API5/API7 живут здесь. |
|
||||
| **Kompas.Mcp.Tests** | Юнит (без COM) + интеграционные (требуют КОМПАС). 138 тестов (83 unit + 55 integration). |
|
||||
|
||||
Принцип: **только `Core` знает про COM**. `Host`/Tools оперируют доменными DTO и вызывают `Core`; `Host` не содержит бизнес-логики.
|
||||
|
||||
@@ -121,7 +121,7 @@ MCP client ──stdio──> Host (поток-пул, async)
|
||||
|
||||
---
|
||||
|
||||
## 5. Карта инструментов (70 инструментов)
|
||||
## 5. Карта инструментов (71 инструмент)
|
||||
|
||||
Сгруппированы вокруг центрального цикла «эскиз ↔ операция». Имена — `snake_case`.
|
||||
|
||||
@@ -204,6 +204,9 @@ MCP client ──stdio──> Host (поток-пул, async)
|
||||
|
||||
**Ограничение:** переменная управляет геометрией только в параметрической модели, где размеры эскизов привязаны к именам переменных. Наши эскизы строятся литеральными координатами — `set_variable` хранит и вычисляет значение, но геометрию не меняет. Связь размеров эскизов с переменными (параметрические эскизы через API2D) — не реализована (package D продолжение).
|
||||
|
||||
**Assembly — построение сборок (`AssemblyService`, `AssemblyTools`, namespace `Kompas.Mcp.Core.Assemblies`)**
|
||||
- `assembly_add_component` — вставить компонент (.m3d деталь или .a3d подсборка) из файла в активную сборку; задать положение origin (x,y,z мм, мировая СК сборки). Возвращает индекс и имя вставленного компонента. Реализовано через API7: `IParts7.AddFromFile(path, ExternalFile=true, Redraw=true)` → `IPart7` → `IPlacement3D.SetOrigin(x,y,z)` → `UpdatePlacement(true)` → `RebuildModel(true)`. `RequireActiveAssembly` проверяет `doc.DocumentType == ksDocumentAssembly` до обращения к `TopPart`. Откат при неуспехе через `IFeature7.Delete`. DI: `AddSingleton<AssemblyService>()` в `Program.cs`.
|
||||
|
||||
**Vision — визуальная обратная связь (fallback)**
|
||||
- `model_snapshot` — отрендерить активный 3D-документ в PNG и вернуть **как image-контент MCP**,
|
||||
чтобы мультимодальный агент *увидел* промежуточный результат. Использовать для визуально-пространственных
|
||||
@@ -337,7 +340,7 @@ dotnet build -c Release -r win-x64 # сборка
|
||||
|
||||
## 10. Дорожная карта
|
||||
|
||||
**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection+package-A+package-B+package-C+package-D start+package-E start+hole+hole_counterbore+hole_countersink+hole_conic+draft):** документы, эскизы (полный набор 2D-примитивов: линия, окружность, дуга, дуга по 3 точкам, прямоугольник, эллипс, ломаная, правильный многоугольник, сплайн NURBS, точка), **вспомогательная геометрия (`sketch_create_on_offset_plane`)**, выдавливание/вырез, вращение, скругление/фаска, **оболочка (`shell`)**, **ребро жёсткости (`rib`)**, **кинематическая операция (`sweep`)**, **операция по сечениям (`loft`)**, **линейный/круговой массив и зеркало (`linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`)**, **отверстие (`hole`, API7)**, **цековка (`hole_counterbore`, API7)**, **зенковка (`hole_countersink`, API7)**, **коническое отверстие (`hole_conic`, API7)**, **уклон (`draft`, API5 `ksInclineDefinition`)**, снимок; `get_part_info`, `get_bounding_box`, `list_faces`, `list_edges`; `import_step`, `export_step`, `list_components`; **`move_face`**, **`split_solid_by_plane`**, **`move_body`**, **`boolean_union`** (прямое редактирование и булевы операции); **`describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure`** (структурный осмотр модели); **`create_variable`, `set_variable`, `delete_variable`** (управление переменными, package D start).
|
||||
**Реализовано (v1+v2+STEP/assembly+direct-edit+inspection+package-A+package-B+package-C+package-D start+package-E start+hole+hole_counterbore+hole_countersink+hole_conic+draft+assembly_add_component):** документы, эскизы (полный набор 2D-примитивов: линия, окружность, дуга, дуга по 3 точкам, прямоугольник, эллипс, ломаная, правильный многоугольник, сплайн NURBS, точка), **вспомогательная геометрия (`sketch_create_on_offset_plane`)**, выдавливание/вырез, вращение, скругление/фаска, **оболочка (`shell`)**, **ребро жёсткости (`rib`)**, **кинематическая операция (`sweep`)**, **операция по сечениям (`loft`)**, **линейный/круговой массив и зеркало (`linear_pattern`, `circular_pattern`, `mirror_operation`, `mirror_body`)**, **отверстие (`hole`, API7)**, **цековка (`hole_counterbore`, API7)**, **зенковка (`hole_countersink`, API7)**, **коническое отверстие (`hole_conic`, API7)**, **уклон (`draft`, API5 `ksInclineDefinition`)**, снимок; `get_part_info`, `get_bounding_box`, `list_faces`, `list_edges`; `import_step`, `export_step`, `list_components`; **`move_face`**, **`split_solid_by_plane`**, **`move_body`**, **`boolean_union`** (прямое редактирование и булевы операции); **`describe_model`, `list_features`, `list_bodies`, `list_variables`, `describe_face`, `describe_edge`, `measure`** (структурный осмотр модели); **`create_variable`, `set_variable`, `delete_variable`** (управление переменными, package D start); **`assembly_add_component`** (вставка компонента в сборку, API7, `AssemblyService`).
|
||||
|
||||
**Следующие приоритеты:**
|
||||
1. Пакет D «параметрика» (продолжение) — связь размеров эскизов с переменными (параметрические эскизы через API2D). ⚠️ **Исследовано**: `ksCDimWithVariable` недоступен из внешней автоматизации — см. `docs/superpowers/specs/2026-05-27-parametric-sketch-findings.md`.
|
||||
@@ -345,7 +348,7 @@ dotnet build -c Release -r win-x64 # сборка
|
||||
3. Рассечение/перемещение тела как MCP-инструменты (`SplitSolids`/`BodyRepositions`) — механика есть, продуктизация не закончена.
|
||||
4. Свойства документа: `IPropertyMng` / `IPropertyKeeper`.
|
||||
5. Полноценное 2D-черчение: виды, линии/дуги/окружности, размеры (`ILinearDimension`, …), штриховки, тексты.
|
||||
6. Спецификации (`ISpecification`), построение сборок и сопряжения.
|
||||
6. Сборки — продолжение: сопряжения (`IMates7`), ограничения, авто-позиционирование компонентов; спецификации (`ISpecification`).
|
||||
7. Транспорт **HTTP/SSE** — как опция для удалённых клиентов.
|
||||
|
||||
---
|
||||
|
||||
@@ -182,3 +182,6 @@ stateful-сессии? Пока полагаемся на последовате
|
||||
|
||||
### ✅ Коническое отверстие — реализовано
|
||||
**Решено.** Новый инструмент `hole_conic` — коническое (конусное) отверстие, сужающееся вглубь. Параметры: `x,y,z`, `diameter` (диаметр у основания), `conicAngle` (угол конуса, 0<angle<180), `depth=0`, `throughAll=false`. Реализован через тот же `HoleCore`-паттерн (`configure`-колбэк): `HoleType=ksHTConic` + configure устанавливает `(IConicHoleParameters)hole.HoleParameters` (`ConicType=ksCNAngle`, `ConicAngle`). Завершает набор типов отверстий: простое (`hole`), цековка (`hole_counterbore`), зенковка (`hole_countersink`), коническое (`hole_conic`). Итог: **70 инструментов, 119 тестов (69 unit + 50 integration).**
|
||||
|
||||
### ✅ Вставка компонента в сборку — реализована (начало класса «Сборки»)
|
||||
**Решено.** Новый инструмент `assembly_add_component(filePath, x, y, z)` — первый инструмент, который *пишет* в сборку (ранее `list_components` только читал). Сервис `AssemblyService` (`src/Kompas.Mcp.Core/Assemblies/`), инструменты — `AssemblyTools`. Паттерн API7: `IParts7.AddFromFile` → `IPlacement3D.SetOrigin` → `UpdatePlacement` → `RebuildModel(true)`. Ключевые тонкости: `UpdatePlacement` возвращает FALSE для вручную позиционируемого компонента — не ошибка; зафиксированный первый компонент двигается через `SetOrigin+RebuildModel` без разфиксации; `RequireActiveAssembly` проверяет тип документа до `TopPart`. Откат при неуспехе через `IFeature7.Delete`. Итог: **71 инструмент, 138 тестов (83 unit + 55 integration).** Следующий шаг: сопряжения (`IMates7`), ограничения.
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
<!-- HERO -->
|
||||
<header class="hero">
|
||||
<div class="wrap">
|
||||
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало+hole+hole_counterbore+hole_countersink+hole_conic+draft+переменные (70 инструментов)</span>
|
||||
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало+hole+hole_counterbore+hole_countersink+hole_conic+draft+переменные+assembly_add_component (71 инструмент)</span>
|
||||
<h1>КОМПАС-3D <span class="g">MCP-сервер</span><br>управление CAD языком LLM</h1>
|
||||
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
|
||||
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
|
||||
@@ -261,7 +261,7 @@
|
||||
впереди.</p>
|
||||
|
||||
<div class="prog">
|
||||
<div class="progrow"><span>Общий прогресс</span><span>v3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell+rib+sweep+loft (пакет B) · массивы+зеркало (пакет C) · переменные (пакет D start) · offset-plane (пакет E start) · hole (API7) · hole_counterbore · hole_countersink · hole_conic · draft (API5) · навык kompas-3d · 70 инструментов · 119 тестов</span></div>
|
||||
<div class="progrow"><span>Общий прогресс</span><span>v3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell+rib+sweep+loft (пакет B) · массивы+зеркало (пакет C) · переменные (пакет D start) · offset-plane (пакет E start) · hole (API7) · hole_counterbore · hole_countersink · hole_conic · draft (API5) · assembly_add_component · навык kompas-3d · 71 инструмент · 138 тестов</span></div>
|
||||
<div class="bar"><i style="width:97%"></i></div>
|
||||
</div>
|
||||
|
||||
@@ -297,7 +297,7 @@
|
||||
<!-- TOOLS -->
|
||||
<div class="panel">
|
||||
<h3>Инструменты v3+ <span class="tag done">ГОТОВО</span></h3>
|
||||
<p class="meta">70 инструментов, отдаются по MCP-протоколу</p>
|
||||
<p class="meta">71 инструмент, отдаются по 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>
|
||||
@@ -309,7 +309,8 @@
|
||||
<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>Variables</code>: <b>create_variable</b> · <b>set_variable</b> · <b>delete_variable</b> (пакет D)</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 wip">→</span><span>пакет E (ось, угловая плоскость) · пакет D параметрика: параметрические эскизы (API2D) — <b>исследовано, недоступно через COM API</b></span></li>
|
||||
<li class="muted"><span class="mark ok">✓</span><span class="t"><code>Assembly</code>: <b>assembly_add_component</b> (вставка компонента .m3d/.a3d в сборку, API7)</span></li>
|
||||
<li><span class="mark wip">→</span><span>пакет E (ось, угловая плоскость) · пакет D параметрика: параметрические эскизы (API2D) — <b>исследовано, недоступно через COM API</b> · сопряжения в сборках</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -320,7 +321,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">Итого 119 тестов зелёных (69 unit + 50 integration)</span></li>
|
||||
<li class="muted"><span class="mark ok">✓</span><span class="t">Итого 138 тестов зелёных (83 unit + 55 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>
|
||||
@@ -347,7 +348,8 @@
|
||||
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">draft · готово ✓</div><h4>Уклон</h4><p><b>draft</b> ✓ (API5 <code>ksInclineDefinition</code>, o3d_incline=42) · 64 инструмента · 115 тестов</p></div>
|
||||
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">пакет D start · готово ✓</div><h4>Переменные модели</h4><p><b>create_variable</b> · <b>set_variable</b> · <b>delete_variable</b> (API5 VariableService) · list_variables исправлен · 67 инструментов</p></div>
|
||||
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">отверстия под крепёж · готово ✓</div><h4>Цековка, зенковка и конус</h4><p><b>hole_counterbore</b> (ksHTCounterbore) · <b>hole_countersink</b> (ksHTCountersinking) · <b>hole_conic</b> (ksHTConic, IConicHoleParameters) · HoleCore рефакторен (configure-колбэк) · 70 инструментов · 119 тестов</p></div>
|
||||
<div class="tl cur"><div class="ph">далее · в работе</div><h4>пакет E продолжение</h4><p>вспомогательная геометрия (ось, угловая плоскость) · пакет D параметрика — параметрические эскизы недоступны через COM API</p></div>
|
||||
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">сборки start · готово ✓</div><h4>Вставка компонентов</h4><p><b>assembly_add_component</b> (API7 IParts7.AddFromFile + IPlacement3D) · 71 инструмент · 138 тестов</p></div>
|
||||
<div class="tl cur"><div class="ph">далее · в работе</div><h4>пакет E продолжение</h4><p>вспомогательная геометрия (ось, угловая плоскость) · сопряжения в сборках · пакет D параметрика — параметрические эскизы недоступны через COM API</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# Дизайн: вставка компонента в сборку (assembly_add_component) через API7
|
||||
|
||||
**Дата:** 2026-05-27
|
||||
**Статус:** реализовано и проверено (3 интеграционных теста зелёные, 136 всего); ревью Codex спека учтено (8 замечаний)
|
||||
|
||||
## Эмпирическая находка при реализации (важно — не разучивать)
|
||||
|
||||
`part.UpdatePlacement(true)` возвращает **FALSE** при перемещении вручную позиционируемого
|
||||
компонента (без сопряжений) — это **НЕ ошибка** (нечего пересчитывать от зависимостей). Само
|
||||
перемещение задаёт `placement.SetOrigin(x,y,z)` (живой объект Placement, не копия — проверено:
|
||||
сдвиг origin на +50 даёт габарит сборки X[50,90]), а **видимым в геометрии** оно становится после
|
||||
`top.RebuildModel(true)`. Поэтому: результат `SetOrigin` проверяем (FALSE = ошибка), результат
|
||||
`UpdatePlacement` **игнорируем**, обязателен `RebuildModel(true)`. Разфиксация первого компонента
|
||||
(`Fixed=false`) **НЕ нужна** — зафиксированный КОМПАСом первый компонент всё равно перемещается
|
||||
через `SetOrigin`+`RebuildModel` (проверено: оба теста — origin и offset — зелёные без снятия
|
||||
фиксации).
|
||||
|
||||
## Цель
|
||||
|
||||
Открыть новый класс функционала — **СБОРКИ**. До сих пор сделана только геометрия ДЕТАЛИ.
|
||||
Первый инкремент вехи: **вставить деталь из сохранённого `.m3d`-файла в активную сборку** с
|
||||
заданием положения. Это фундамент — без вставки компонентов сборка пуста.
|
||||
|
||||
`list_components` (чтение сборки) уже есть; теперь учимся её НАПОЛНЯТЬ.
|
||||
|
||||
## Архитектурное решение
|
||||
|
||||
Сборки — это **НЕ `PartModeler`** (он строит API5-деталь `ksPart`). Заводим **новый сервис
|
||||
`AssemblyService`** (API7, паттерн как у `HoleService`/`FaceEditService`: контейнеры берём
|
||||
COM-QI, операции на STA-потоке `KompasDispatcher`) и **новый класс инструментов `AssemblyTools`**.
|
||||
|
||||
- `src/Kompas.Mcp.Core/Assemblies/AssemblyService.cs` — сервис.
|
||||
- `src/Kompas.Mcp.Core/Assemblies/AssemblyValidation.cs` — чистые static-валидаторы (unit-тест).
|
||||
- `src/Kompas.Mcp.Core/Assemblies/AddedComponent.cs` — record результата (Index, Name).
|
||||
- `src/Kompas.Mcp.Host/Tools/AssemblyTools.cs` — MCP-инструмент.
|
||||
- DI: `AddSingleton<AssemblyService>()` в `Program.cs`.
|
||||
|
||||
(Namespace `Kompas.Mcp.Core.Assemblies` — во множественном числе, чтобы не конфликтовать с
|
||||
`System.Reflection.Assembly`.)
|
||||
|
||||
## Подтверждённый workflow (рефлексия interop + справка)
|
||||
|
||||
Рефлексией `KompasAPI7.dll` подтверждено:
|
||||
- `IParts7.AddFromFile(string FileName, bool ExternalFile, bool Redraw)` → `Part7`.
|
||||
- `IPart7.Placement` (`Placement3D`), `IPart7.UpdatePlacement(bool Redraw)`.
|
||||
- `IPlacement3D.SetOrigin(double X, double Y, double Z)`.
|
||||
|
||||
```
|
||||
// RequireActiveAssembly (см. ниже) — раздельные проверки до доступа к TopPart:
|
||||
IKompasDocument doc = app.ActiveDocument; // != null
|
||||
// doc.DocumentType == ksDocumentAssembly (деталь тоже IKompasDocument3D — отсечь по типу!)
|
||||
IKompasDocument3D doc3d = (IKompasDocument3D)doc;
|
||||
IPart7 top = (IPart7)doc3d.TopPart; // != null
|
||||
IParts7 parts = top.Parts;
|
||||
|
||||
IPart7 part = parts.AddFromFile(filePath, /*ExternalFile*/ true, /*Redraw*/ true) as IPart7
|
||||
?? throw; // проверяем результат
|
||||
// ExternalFile=true: компонент — ссылка на внешний .m3d (что нам и нужно — отдельный файл-источник).
|
||||
// Redraw=true: вставить и перерисовать ИСХОДНОЕ положение (в начало координат сборки).
|
||||
|
||||
IPlacement3D pl = part.Placement as IPlacement3D ?? throw; // read-only свойство, мутируем
|
||||
if (!pl.SetOrigin(x, y, z)) throw; // мировые координаты сборки, мм
|
||||
if (!part.UpdatePlacement(/*Redraw*/ true)) throw; // именно это ПРИМЕНЯЕТ перемещение
|
||||
```
|
||||
|
||||
**Проверки результатов (как в `HoleService`):** `AddFromFile` приводится к `IPart7` (иначе ошибка);
|
||||
`Placement` приводится к `IPlacement3D`; `SetOrigin(...)==true`; `UpdatePlacement(true)==true`.
|
||||
Неуспех любого — `InvalidOperationException` с понятным текстом. (Откат «осиротевшего» компонента
|
||||
при неуспехе позиционирования — best-effort через `part.Owner?.Delete()`; на практике origin-сдвиг
|
||||
надёжен, но проверки обязательны.)
|
||||
|
||||
**Redraw ≠ применение перемещения (важно):** `AddFromFile(..., Redraw:true)` вставляет и
|
||||
перерисовывает компонент в ИСХОДНОМ положении. Само перемещение применяет и проверяет именно
|
||||
`part.UpdatePlacement(true)` — не `Redraw`. Отдельный `doc3d.RebuildDocument()` в базовом workflow
|
||||
не нужен (интеграционный тест подтверждает габарит после `UpdatePlacement`).
|
||||
|
||||
**Почему `AddFromFile`, а не `AddFromFileWithParam`:** для первого инкремента нужен только сдвиг
|
||||
origin. `AddFromFile` + `Placement.SetOrigin` + `UpdatePlacement` — минимальный надёжный путь
|
||||
(подтверждён рефлексией: все три члена есть в interop). `AddFromFileWithParam` требует получить
|
||||
`IPlacement3D`/`IInsertPartParameters` через `IKompasDocument1.GetInterface(код)` до вставки — это
|
||||
лишняя неочевидность (коды 11223/13196 не проверены рефлексией) и YAGNI на этом шаге. Ориентацию
|
||||
(`SetVector`/углы Эйлера) и параметры вставки добавим в следующих инкрементах при необходимости.
|
||||
|
||||
**Единицы:** `x/y/z` — миллиметры в мировой СК сборки. По умолчанию (0,0,0) компонент встаёт в
|
||||
начало координат сборки.
|
||||
|
||||
**Фиксация:** через API первый компонент НЕ фиксируется автоматически. На этом шаге фиксацию НЕ
|
||||
выставляем (YAGNI) — для теста объёмом/габаритом она не нужна; добавим параметр `fixed` позже,
|
||||
если понадобится.
|
||||
|
||||
## MCP-инструмент (новая группа Assembly)
|
||||
|
||||
| Инструмент | Параметры | Поведение |
|
||||
|---|---|---|
|
||||
| `assembly_add_component` | `filePath: string`, `x=0`, `y=0`, `z=0` | Вставить деталь/подсборку из `.m3d`/`.a3d`-файла в активную сборку; origin компонента — (x,y,z) мм. Возвращает индекс и имя вставленного компонента. |
|
||||
|
||||
Контракт: активный документ должен быть **сборкой** (иначе понятная ошибка); файл должен
|
||||
существовать. Возврат — текст вида «Компонент '<имя>' вставлен в сборку, индекс N, позиция (x,y,z)».
|
||||
|
||||
## Валидация (чистые static, unit-тест)
|
||||
|
||||
`AssemblyValidation`:
|
||||
- `NormalizeComponentPath(string? path) → string` — `Trim` → проверка непустоты → проверка
|
||||
расширения (`.m3d`/`.a3d`, case-insensitive; иначе `ArgumentException`) → `Path.GetFullPath`
|
||||
(вернуть абсолютный путь). Чистая (не трогает ФС) → unit-тест.
|
||||
- `RequireFinitePosition(double x, double y, double z)` — координаты конечны (`double.IsFinite`;
|
||||
отсекает `Infinity`/`NaN`, как в `HoleService`).
|
||||
|
||||
Проверка существования файла (`File.Exists` на нормализованном пути) — в сервисе (не чистая, не
|
||||
unit-тест), даёт `FileNotFoundException` с абсолютным путём.
|
||||
|
||||
## Проверка активного документа (helper в сервисе)
|
||||
|
||||
`RequireActiveAssembly()` — раздельные ошибки (Codex #2):
|
||||
1. `ActiveDocument == null` → «Нет активного документа».
|
||||
2. `doc.DocumentType != ksDocumentAssembly` → «Активный документ не сборка (тип: …). Создайте
|
||||
сборку: document_create assembly». (Деталь — тоже `IKompasDocument3D`, поэтому проверка по типу
|
||||
обязательна ДО доступа к `TopPart`.)
|
||||
3. `doc is not IKompasDocument3D` → «Активный документ не 3D».
|
||||
4. `doc3d.TopPart is not IPart7` → «Не удалось получить TopPart сборки».
|
||||
|
||||
## Реализация
|
||||
|
||||
- `AssemblyService.AddComponentAsync(string filePath, double x, double y, double z, ct)` —
|
||||
на STA-потоке: валидация → проверка типа активного документа (сборка) → `AddFromFile` →
|
||||
`Placement.SetOrigin` → `UpdatePlacement` → вернуть `AddedComponent(Index, Name)`.
|
||||
Индекс = `parts.Count - 1` после вставки; имя = `part.Name`.
|
||||
- Транзитные RCW не освобождаем точечно — консистентно с остальным слоем (долг v2-2).
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Unit (`AssemblyValidationTests`)
|
||||
- `NormalizeComponentPath` бросает на null/пустой/пробельный; бросает на чужом расширении
|
||||
(`.step`/`.txt`); пропускает `.m3d`/`.a3d` (в т.ч. с пробелами и в разном регистре), возвращает
|
||||
абсолютный путь.
|
||||
- `RequireFinitePosition` бросает на NaN/Infinity по каждой координате; пропускает конечные.
|
||||
|
||||
### Integration (`AssemblyTests`, Collection KompasCollection)
|
||||
Паттерн: построить деталь → `SaveAsAsync(.scratch/*.m3d)` → **проверить `File.Exists` + размер>0** →
|
||||
`CloseAsync` → создать сборку → `AddComponentAsync` → проверить → `finally CloseAsync`.
|
||||
**Геометрия детали фиксирована от нуля** (эскиз 0..40 × 0..40, выдавить 0..20 ⇒ локальный габарит
|
||||
X[0,40] Y[0,40] Z[0,20]) — чтобы абсолютные ожидания по габариту были детерминированы (Codex #7).
|
||||
|
||||
1. **Вставка в начало координат:** сохранить деталь-коробку, проверить файл, закрыть. Создать
|
||||
сборку. `before = list_components.Count`. `var c = AddComponentAsync(file, 0,0,0)`.
|
||||
- `list_components.Count == before + 1` (Codex #6); компонент по индексу `c.Index`:
|
||||
`SizeX≈40, SizeY≈40, SizeZ≈20` (±допуск); `c.Name` непуст.
|
||||
- `get_bounding_box` сборки: X∈[0,40], Y∈[0,40], Z∈[0,20] (±допуск).
|
||||
2. **Позиционирование:** та же деталь, `AddComponentAsync(file, 50, 0, 0)`.
|
||||
- `get_bounding_box` сборки: X∈[50,90] (сдвиг origin на +50 подтверждает работу позиции).
|
||||
|
||||
Объёмом сборку тоже можно проверить (`get_part_info` на TopPart агрегирует компоненты) —
|
||||
но габарит надёжнее и прямо проверяет позицию. Артефакты `.m3d` — в gitignored `.scratch/`.
|
||||
|
||||
## Дальнейшие инкременты (вне этого спека)
|
||||
2. `assembly_add_mate` — сопряжения (`IMateConstraints3D.Add(mc_Coincidence/mc_Distance/…)` +
|
||||
`BaseObject1/2` + `Update`). Типы: совпадение/параллельность/перпендикулярность/касательность/
|
||||
концентричность/расстояние/угол/симметрия.
|
||||
3. Перемещение/массив/фиксация компонентов — по необходимости.
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Kompas.Mcp.Core.Assemblies;
|
||||
|
||||
/// <summary>Результат вставки компонента в сборку.</summary>
|
||||
public sealed record AddedComponent
|
||||
{
|
||||
/// <summary>Индекс вставленного компонента в коллекции компонентов сборки.</summary>
|
||||
public required int Index { get; init; }
|
||||
|
||||
/// <summary>Имя вставленного компонента в дереве сборки.</summary>
|
||||
public required string Name { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
using System.Runtime.Versioning;
|
||||
using Kompas.Mcp.Core.Threading;
|
||||
using Kompas6Constants;
|
||||
using KompasAPI7;
|
||||
|
||||
namespace Kompas.Mcp.Core.Assemblies;
|
||||
|
||||
/// <summary>
|
||||
/// Операции со сборками через API7 (<c>IParts7</c>). Аналогично <c>HoleService</c>/<c>FaceEditService</c>:
|
||||
/// контейнеры берутся COM-QI, все вызовы — на STA-потоке <see cref="KompasDispatcher"/>.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class AssemblyService
|
||||
{
|
||||
private readonly KompasSession _session;
|
||||
private readonly KompasDispatcher _dispatcher;
|
||||
|
||||
public AssemblyService(KompasSession session, KompasDispatcher dispatcher)
|
||||
{
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Вставить компонент (деталь .m3d или подсборку .a3d) из файла в активную сборку, разместив
|
||||
/// его origin в мировой точке (<paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>) мм.
|
||||
/// </summary>
|
||||
public Task<AddedComponent> AddComponentAsync(string filePath, double x, double y, double z, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => AddComponentCore(filePath, x, y, z), ct);
|
||||
|
||||
private AddedComponent AddComponentCore(string filePath, double x, double y, double z)
|
||||
{
|
||||
var path = AssemblyValidation.NormalizeComponentPath(filePath);
|
||||
AssemblyValidation.RequireFinitePosition(x, y, z);
|
||||
if (!File.Exists(path))
|
||||
throw new FileNotFoundException($"Файл компонента не найден: {path}", path);
|
||||
|
||||
var top = RequireActiveAssembly();
|
||||
var parts = top.Parts
|
||||
?? throw new InvalidOperationException("Не удалось получить коллекцию компонентов сборки (Parts).");
|
||||
|
||||
// Фиксируем число компонентов ДО вставки — для верифицируемого отката (откатываем всё, что
|
||||
// появилось сверх beforeCount), не полагаясь на возможный null-возврат AddFromFile.
|
||||
int beforeCount = parts.Count;
|
||||
|
||||
// ExternalFile=true: компонент ссылается на внешний .m3d/.a3d. Redraw=true: вставить и
|
||||
// перерисовать в исходном положении (само перемещение применит SetOrigin+RebuildModel ниже).
|
||||
var part = parts.AddFromFile(path, true, true) as IPart7;
|
||||
if (part is null)
|
||||
{
|
||||
// AddFromFile мог вставить компонент и всё равно вернуть null/не-IPart7 — почистим, если
|
||||
// число компонентов выросло, чтобы не оставить «осиротевший» компонент в сборке.
|
||||
RollbackInsertion(top, parts, beforeCount);
|
||||
throw new InvalidOperationException(
|
||||
$"Не удалось вставить компонент из файла (AddFromFile вернул null или не IPart7): {path}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var placement = part.Placement as IPlacement3D
|
||||
?? throw new InvalidOperationException("Placement компонента не приводится к IPlacement3D.");
|
||||
if (!placement.SetOrigin(x, y, z))
|
||||
throw new InvalidOperationException("SetOrigin вернул FALSE — не удалось задать положение компонента.");
|
||||
|
||||
// UpdatePlacement применяет изменение Placement; для вручную позиционируемого компонента
|
||||
// (без сопряжений/зависимостей) он может вернуть FALSE — это НЕ ошибка (нечего
|
||||
// пересчитывать). Фактически положение становится видимым в геометрии после RebuildModel.
|
||||
part.UpdatePlacement(true);
|
||||
if (!top.RebuildModel(true))
|
||||
throw new InvalidOperationException("RebuildModel вернул FALSE — сборка не перестроена после вставки компонента.");
|
||||
|
||||
return new AddedComponent { Index = parts.Count - 1, Name = SafeName(part) };
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Верифицируемый откол: удаляем всё, что добавилось сверх beforeCount, и проверяем.
|
||||
RollbackInsertion(top, parts, beforeCount);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Откатить вставку: удалить компоненты, появившиеся сверх <paramref name="beforeCount"/> (обычно
|
||||
/// один — только что вставленный). Best-effort, но с верификацией: компонент удаляется через
|
||||
/// <c>IFeature7.Delete()</c> своего владельца, затем сборка перестраивается.
|
||||
/// </summary>
|
||||
private static void RollbackInsertion(IPart7 top, IParts7 parts, int beforeCount)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Удаляем с конца, пока число не вернётся к исходному (защита от частичной вставки).
|
||||
for (int guard = 0; parts.Count > beforeCount && guard < 8; guard++)
|
||||
{
|
||||
if (parts.Part[parts.Count - 1] is IPart7 last && last.Owner is { } owner)
|
||||
owner.Delete();
|
||||
else
|
||||
break; // не удалось получить владельца — выходим, чтобы не зациклиться
|
||||
}
|
||||
top.RebuildModel(true);
|
||||
}
|
||||
catch { /* лучшее усилие: исходное исключение важнее ошибки отката */ }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить верхний компонент активной сборки, проверив тип документа. Раздельные ошибки:
|
||||
/// нет документа / документ не сборка / не 3D / нет TopPart. Деталь — тоже IKompasDocument3D,
|
||||
/// поэтому отсекаем по DocumentType ДО доступа к TopPart.
|
||||
/// </summary>
|
||||
private IPart7 RequireActiveAssembly()
|
||||
{
|
||||
var doc = _session.Application.ActiveDocument
|
||||
?? throw new InvalidOperationException("Нет активного документа. Создайте сборку: document_create assembly.");
|
||||
if (doc.DocumentType != DocumentTypeEnum.ksDocumentAssembly)
|
||||
throw new InvalidOperationException(
|
||||
$"Активный документ не является сборкой (тип: {doc.DocumentType}). Создайте сборку: document_create assembly.");
|
||||
if (doc is not IKompasDocument3D doc3d)
|
||||
throw new InvalidOperationException("Активный документ не является 3D-документом.");
|
||||
return doc3d.TopPart as IPart7
|
||||
?? throw new InvalidOperationException("Не удалось получить TopPart сборки.");
|
||||
}
|
||||
|
||||
private static string SafeName(IPart7 part)
|
||||
{
|
||||
try { return part.Name ?? string.Empty; }
|
||||
catch { return string.Empty; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace Kompas.Mcp.Core.Assemblies;
|
||||
|
||||
/// <summary>
|
||||
/// Чистые валидаторы операций сборки (без COM) — выделены для unit-тестов.
|
||||
/// </summary>
|
||||
public static class AssemblyValidation
|
||||
{
|
||||
/// <summary>Допустимые расширения файла-источника компонента: деталь (.m3d) и сборка (.a3d).</summary>
|
||||
private static readonly HashSet<string> AllowedExtensions =
|
||||
new(StringComparer.OrdinalIgnoreCase) { ".m3d", ".a3d" };
|
||||
|
||||
/// <summary>
|
||||
/// Нормализовать путь к файлу компонента: обрезать пробелы, проверить непустоту и расширение
|
||||
/// (.m3d/.a3d, без учёта регистра), вернуть абсолютный путь. Существование файла НЕ проверяет
|
||||
/// (это делает сервис на реальной ФС).
|
||||
/// </summary>
|
||||
public static string NormalizeComponentPath(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
throw new ArgumentException("Путь к файлу компонента не задан.", nameof(path));
|
||||
|
||||
var trimmed = path.Trim();
|
||||
var ext = Path.GetExtension(trimmed);
|
||||
if (!AllowedExtensions.Contains(ext))
|
||||
throw new ArgumentException(
|
||||
$"Недопустимое расширение файла компонента '{ext}'. Ожидается .m3d (деталь) или .a3d (сборка).",
|
||||
nameof(path));
|
||||
|
||||
return Path.GetFullPath(trimmed);
|
||||
}
|
||||
|
||||
/// <summary>Проверить, что координаты вставки конечны (отсекает NaN/Infinity).</summary>
|
||||
public static void RequireFinitePosition(double x, double y, double z)
|
||||
{
|
||||
if (!double.IsFinite(x) || !double.IsFinite(y) || !double.IsFinite(z))
|
||||
throw new ArgumentException("Координаты вставки компонента должны быть конечными числами (не NaN/Infinity).");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Kompas.Mcp.Core;
|
||||
using Kompas.Mcp.Core.Assemblies;
|
||||
using Kompas.Mcp.Core.Conversion;
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Editing;
|
||||
@@ -23,6 +24,7 @@ builder.Services.AddSingleton<KompasSession>();
|
||||
builder.Services.AddSingleton<DocumentService>();
|
||||
builder.Services.AddSingleton<PartModeler>();
|
||||
builder.Services.AddSingleton<HoleService>();
|
||||
builder.Services.AddSingleton<AssemblyService>();
|
||||
builder.Services.AddSingleton<VariableService>();
|
||||
builder.Services.AddSingleton<SnapshotService>();
|
||||
builder.Services.AddSingleton<QueryService>();
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.ComponentModel;
|
||||
using Kompas.Mcp.Core;
|
||||
using Kompas.Mcp.Core.Assemblies;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace Kompas.Mcp.Host.Tools;
|
||||
|
||||
/// <summary>Инструменты работы со сборками (вставка/сопряжение компонентов).</summary>
|
||||
[McpServerToolType]
|
||||
public sealed class AssemblyTools(KompasSession session, AssemblyService assembly)
|
||||
{
|
||||
[McpServerTool(Name = "assembly_add_component")]
|
||||
[Description("Вставить компонент (деталь .m3d или подсборку .a3d) из файла в активную сборку. " +
|
||||
"Активный документ должен быть сборкой (document_create assembly). origin компонента — точка (x,y,z) в мм " +
|
||||
"мировой СК сборки (по умолчанию начало координат). Деталь-источник должна быть сохранена в файл " +
|
||||
"(document_save_as). Возвращает индекс и имя вставленного компонента.")]
|
||||
public async Task<string> AddComponent(
|
||||
[Description("Путь к файлу-источнику компонента (.m3d/.a3d)")] string filePath,
|
||||
[Description("X положения origin компонента, мм")] double x = 0,
|
||||
[Description("Y положения origin компонента, мм")] double y = 0,
|
||||
[Description("Z положения origin компонента, мм")] double z = 0)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
var c = await assembly.AddComponentAsync(filePath, x, y, z);
|
||||
return $"Компонент '{c.Name}' вставлен в сборку, индекс {c.Index}, позиция ({x}, {y}, {z}).";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Kompas.Mcp.Core.Assemblies;
|
||||
|
||||
namespace Kompas.Mcp.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AssemblyValidationTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void NormalizeComponentPath_throws_on_empty(string? path)
|
||||
=> Assert.Throws<ArgumentException>(() => AssemblyValidation.NormalizeComponentPath(path));
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"C:\parts\bolt.step")]
|
||||
[InlineData(@"C:\parts\bolt.txt")]
|
||||
[InlineData(@"C:\parts\bolt")]
|
||||
public void NormalizeComponentPath_throws_on_wrong_extension(string path)
|
||||
=> Assert.Throws<ArgumentException>(() => AssemblyValidation.NormalizeComponentPath(path));
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"C:\parts\bolt.m3d", ".m3d")]
|
||||
[InlineData(@"C:\parts\frame.a3d", ".a3d")]
|
||||
[InlineData(@"C:\parts\bolt.M3D", ".m3d")] // регистр не важен
|
||||
[InlineData(" C:\\parts\\bolt.a3d ", ".a3d")] // пробелы обрезаются
|
||||
public void NormalizeComponentPath_accepts_m3d_and_a3d(string path, string expectedExt)
|
||||
{
|
||||
var result = AssemblyValidation.NormalizeComponentPath(path);
|
||||
Assert.True(Path.IsPathFullyQualified(result), "Должен вернуться абсолютный путь.");
|
||||
Assert.EndsWith(expectedExt, result, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(double.NaN, 0, 0)]
|
||||
[InlineData(0, double.PositiveInfinity, 0)]
|
||||
[InlineData(0, 0, double.NegativeInfinity)]
|
||||
public void RequireFinitePosition_throws_on_non_finite(double x, double y, double z)
|
||||
=> Assert.Throws<ArgumentException>(() => AssemblyValidation.RequireFinitePosition(x, y, z));
|
||||
|
||||
[Fact]
|
||||
public void RequireFinitePosition_accepts_finite()
|
||||
=> Assert.Null(Record.Exception(() => AssemblyValidation.RequireFinitePosition(50, -10, 0)));
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
using Kompas.Mcp.Core.Assemblies;
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
using Kompas.Mcp.Core.Query;
|
||||
|
||||
namespace Kompas.Mcp.Tests.Integration;
|
||||
|
||||
/// <summary>Интеграция: вставка компонента в сборку (API7 IParts7.AddFromFile + Placement).</summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Collection(KompasCollection.Name)]
|
||||
public sealed class AssemblyTests
|
||||
{
|
||||
private readonly DocumentService _docs;
|
||||
private readonly PartModeler _modeler;
|
||||
private readonly AssemblyService _assembly;
|
||||
private readonly QueryService _query;
|
||||
|
||||
public AssemblyTests(KompasFixture fx)
|
||||
{
|
||||
_docs = new DocumentService(fx.Session, fx.Dispatcher);
|
||||
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
|
||||
_assembly = new AssemblyService(fx.Session, fx.Dispatcher);
|
||||
_query = new QueryService(fx.Session, fx.Dispatcher);
|
||||
}
|
||||
|
||||
/// <summary>Построить деталь-коробку от нуля (габарит X[0,w] Y[0,d] Z[0,h]), сохранить в .m3d,
|
||||
/// проверить файл, закрыть. Возвращает абсолютный путь к сохранённому файлу.</summary>
|
||||
private async Task<string> BuildAndSaveBoxAsync(double w, double d, double h)
|
||||
{
|
||||
var path = TestPaths.NewFile(".m3d");
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddRectangleAsync(s, 0, 0, w, d);
|
||||
await _modeler.CloseSketchAsync(s);
|
||||
await _modeler.ExtrudeAsync(s, depth: h);
|
||||
await _modeler.RebuildAsync();
|
||||
await _docs.SaveAsAsync(path);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
|
||||
// Отделяем проблемы сохранения от проблем вставки: файл должен реально записаться.
|
||||
Assert.True(File.Exists(path), $"Файл детали не сохранён: {path}");
|
||||
Assert.True(new FileInfo(path).Length > 0, "Файл детали пуст.");
|
||||
return path;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddComponent_inserts_one_component_at_origin()
|
||||
{
|
||||
var partFile = await BuildAndSaveBoxAsync(40, 40, 20);
|
||||
|
||||
await _docs.CreateAsync(KompasDocumentType.Assembly);
|
||||
try
|
||||
{
|
||||
var before = (await _query.ListComponentsAsync()).Count;
|
||||
|
||||
var added = await _assembly.AddComponentAsync(partFile, 0, 0, 0);
|
||||
|
||||
var comps = await _query.ListComponentsAsync();
|
||||
Assert.Equal(before + 1, comps.Count);
|
||||
Assert.False(string.IsNullOrWhiteSpace(added.Name), "Имя вставленного компонента не должно быть пустым.");
|
||||
|
||||
var c = comps[added.Index];
|
||||
Assert.InRange(c.SizeX, 40 * 0.95, 40 * 1.05);
|
||||
Assert.InRange(c.SizeY, 40 * 0.95, 40 * 1.05);
|
||||
Assert.InRange(c.SizeZ, 20 * 0.95, 20 * 1.05);
|
||||
|
||||
// Габарит сборки совпадает с деталью, вставленной в начало координат.
|
||||
var bb = await _query.GetBoundingBoxAsync();
|
||||
Assert.InRange(bb.MinX, -1, 1);
|
||||
Assert.InRange(bb.MaxX, 39, 41);
|
||||
Assert.InRange(bb.MinY, -1, 1);
|
||||
Assert.InRange(bb.MaxY, 39, 41);
|
||||
Assert.InRange(bb.MinZ, -1, 1);
|
||||
Assert.InRange(bb.MaxZ, 19, 21);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddComponent_positions_component_at_offset()
|
||||
{
|
||||
var partFile = await BuildAndSaveBoxAsync(40, 40, 20);
|
||||
|
||||
await _docs.CreateAsync(KompasDocumentType.Assembly);
|
||||
try
|
||||
{
|
||||
// Вставка со сдвигом origin на +50 по X → габарит сборки X[50,90].
|
||||
await _assembly.AddComponentAsync(partFile, 50, 0, 0);
|
||||
|
||||
var bb = await _query.GetBoundingBoxAsync();
|
||||
Assert.InRange(bb.MinX, 49, 51);
|
||||
Assert.InRange(bb.MaxX, 89, 91);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddComponent_into_non_empty_assembly_increments_and_indexes()
|
||||
{
|
||||
// Вставка ВТОРОГО компонента в непустую сборку: проверяет инкремент count и что
|
||||
// возвращённый Index указывает именно на вставленный компонент (а не «последний по случаю»).
|
||||
var partFile = await BuildAndSaveBoxAsync(30, 30, 10);
|
||||
|
||||
await _docs.CreateAsync(KompasDocumentType.Assembly);
|
||||
try
|
||||
{
|
||||
await _assembly.AddComponentAsync(partFile, 0, 0, 0); // первый
|
||||
var before = (await _query.ListComponentsAsync()).Count;
|
||||
Assert.Equal(1, before);
|
||||
|
||||
var added = await _assembly.AddComponentAsync(partFile, 60, 0, 0); // второй, со сдвигом
|
||||
|
||||
var comps = await _query.ListComponentsAsync();
|
||||
Assert.Equal(before + 1, comps.Count);
|
||||
Assert.Equal(comps.Count - 1, added.Index);
|
||||
// Компонент по возвращённому индексу — деталь нужного габарита.
|
||||
Assert.InRange(comps[added.Index].SizeX, 30 * 0.95, 30 * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddComponent_missing_file_throws_and_keeps_assembly_empty()
|
||||
{
|
||||
var missing = Path.Combine(TestPaths.Scratch, $"no_such_{Guid.NewGuid():N}.m3d");
|
||||
|
||||
await _docs.CreateAsync(KompasDocumentType.Assembly);
|
||||
try
|
||||
{
|
||||
await Assert.ThrowsAsync<FileNotFoundException>(
|
||||
() => _assembly.AddComponentAsync(missing, 0, 0, 0));
|
||||
|
||||
// Сборка не должна получить «осиротевший» компонент от неуспешной вставки.
|
||||
Assert.Empty(await _query.ListComponentsAsync());
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddComponent_rejects_non_assembly_document()
|
||||
{
|
||||
// Активный документ — деталь (не сборка): должна быть понятная ошибка.
|
||||
var partFile = await BuildAndSaveBoxAsync(20, 20, 10);
|
||||
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => _assembly.AddComponentAsync(partFile, 0, 0, 0));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user