Проверки COM-результатов (AddFromFile/SetOrigin/UpdatePlacement), helper RequireActiveAssembly (раздельные ошибки, проверка типа документа до TopPart), валидация расширения .m3d/.a3d + нормализация пути, before/after count в тесте, фиксированная геометрия детали от нуля, проверка сохранённости файла. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
12 KiB
Дизайн: вставка компонента в сборку (assembly_add_component) через API7
Дата: 2026-05-27 Статус: спроектировано (сигнатуры подтверждены рефлексией interop + kompas-sdk-research); ревью Codex учтено (8 замечаний внесены)
Цель
Открыть новый класс функционала — СБОРКИ. До сих пор сделана только геометрия ДЕТАЛИ.
Первый инкремент вехи: вставить деталь из сохранённого .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):
ActiveDocument == null→ «Нет активного документа».doc.DocumentType != ksDocumentAssembly→ «Активный документ не сборка (тип: …). Создайте сборку: document_create assembly». (Деталь — тожеIKompasDocument3D, поэтому проверка по типу обязательна ДО доступа кTopPart.)doc is not IKompasDocument3D→ «Активный документ не 3D».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).
- Вставка в начало координат: сохранить деталь-коробку, проверить файл, закрыть. Создать
сборку.
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] (±допуск).
- Позиционирование: та же деталь,
AddComponentAsync(file, 50, 0, 0).get_bounding_boxсборки: X∈[50,90] (сдвиг origin на +50 подтверждает работу позиции).
Объёмом сборку тоже можно проверить (get_part_info на TopPart агрегирует компоненты) —
но габарит надёжнее и прямо проверяет позицию. Артефакты .m3d — в gitignored .scratch/.
Дальнейшие инкременты (вне этого спека)
assembly_add_mate— сопряжения (IMateConstraints3D.Add(mc_Coincidence/mc_Distance/…)+BaseObject1/2+Update). Типы: совпадение/параллельность/перпендикулярность/касательность/ концентричность/расстояние/угол/симметрия.- Перемещение/массив/фиксация компонентов — по необходимости.