5eee7ad1af
Веха СБОРКИ, инкремент 1. Новый AssemblyService (API7, паттерн HoleService): IParts7.AddFromFile → Placement.SetOrigin → RebuildModel. Новый AssemblyTools (MCP-инструмент assembly_add_component), регистрация в DI. Ключевая находка: UpdatePlacement возвращает FALSE для вручную позиционируемого компонента (не ошибка), позицию применяет SetOrigin + RebuildModel(true). Валидация — чистый AssemblyValidation (расширение .m3d/.a3d, нормализация пути, конечность координат). RequireActiveAssembly — раздельные ошибки по типу документа. Тесты: +14 unit (AssemblyValidation), +3 integration (AssemblyTests: вставка в origin, позиционирование со сдвигом, отклонение не-сборки). Итого 83 unit + 53 integration = 136 зелёных. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
13 KiB
Markdown
160 lines
13 KiB
Markdown
# Дизайн: вставка компонента в сборку (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. Перемещение/массив/фиксация компонентов — по необходимости.
|