Files
kompas3d-mcp/docs/superpowers/specs/2026-05-27-assembly-add-component-design.md
T
mikhail 5eee7ad1af feat: assembly_add_component — вставка детали из .m3d в сборку (API7)
Веха СБОРКИ, инкремент 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>
2026-05-27 13:04:19 +03:00

160 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Дизайн: вставка компонента в сборку (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. Перемещение/массив/фиксация компонентов — по необходимости.