docs: спек assembly_add_component — учтены 8 замечаний ревью Codex
Проверки COM-результатов (AddFromFile/SetOrigin/UpdatePlacement), helper RequireActiveAssembly (раздельные ошибки, проверка типа документа до TopPart), валидация расширения .m3d/.a3d + нормализация пути, before/after count в тесте, фиксированная геометрия детали от нуля, проверка сохранённости файла. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Дизайн: вставка компонента в сборку (assembly_add_component) через API7
|
||||
|
||||
**Дата:** 2026-05-27
|
||||
**Статус:** спроектировано (сигнатуры подтверждены рефлексией interop + kompas-sdk-research); на ревью (Codex)
|
||||
**Статус:** спроектировано (сигнатуры подтверждены рефлексией interop + kompas-sdk-research); ревью Codex учтено (8 замечаний внесены)
|
||||
|
||||
## Цель
|
||||
|
||||
@@ -34,20 +34,34 @@ COM-QI, операции на STA-потоке `KompasDispatcher`) и **новы
|
||||
- `IPlacement3D.SetOrigin(double X, double Y, double Z)`.
|
||||
|
||||
```
|
||||
IKompasDocument3D doc3d = (IKompasDocument3D)app.ActiveDocument; // должна быть СБОРКА
|
||||
// валидация: ((IKompasDocument)doc3d).DocumentType == ksDocumentAssembly
|
||||
IPart7 top = (IPart7)doc3d.TopPart;
|
||||
IParts7 parts = top.Parts; // COM-свойство
|
||||
// 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 = (IPart7)parts.AddFromFile(filePath, /*ExternalFile*/ true, /*Redraw*/ true);
|
||||
IPart7 part = parts.AddFromFile(filePath, /*ExternalFile*/ true, /*Redraw*/ true) as IPart7
|
||||
?? throw; // проверяем результат
|
||||
// ExternalFile=true: компонент — ссылка на внешний .m3d (что нам и нужно — отдельный файл-источник).
|
||||
// Redraw=true: перестроить сразу (отдельный RebuildModel не нужен).
|
||||
// Redraw=true: вставить и перерисовать ИСХОДНОЕ положение (в начало координат сборки).
|
||||
|
||||
IPlacement3D pl = (IPlacement3D)part.Placement; // read-only свойство, мутируем
|
||||
pl.SetOrigin(x, y, z); // мировые координаты сборки, мм
|
||||
part.UpdatePlacement(/*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` требует получить
|
||||
@@ -74,12 +88,24 @@ origin. `AddFromFile` + `Placement.SetOrigin` + `UpdatePlacement` — миним
|
||||
## Валидация (чистые static, unit-тест)
|
||||
|
||||
`AssemblyValidation`:
|
||||
- `RequireComponentPath(string? path)` — путь непустой (формат строки).
|
||||
- `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` с путём.
|
||||
Проверка существования файла (`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 сборки».
|
||||
|
||||
## Реализация
|
||||
|
||||
@@ -92,19 +118,24 @@ origin. `AddFromFile` + `Placement.SetOrigin` + `UpdatePlacement` — миним
|
||||
## Тестирование
|
||||
|
||||
### Unit (`AssemblyValidationTests`)
|
||||
- `RequireComponentPath` бросает на null/пустой/пробельный; пропускает валидный.
|
||||
- `NormalizeComponentPath` бросает на null/пустой/пробельный; бросает на чужом расширении
|
||||
(`.step`/`.txt`); пропускает `.m3d`/`.a3d` (в т.ч. с пробелами и в разном регистре), возвращает
|
||||
абсолютный путь.
|
||||
- `RequireFinitePosition` бросает на NaN/Infinity по каждой координате; пропускает конечные.
|
||||
|
||||
### Integration (`AssemblyTests`, Collection KompasCollection)
|
||||
Паттерн: построить деталь → `SaveAsAsync(.scratch/*.m3d)` → `CloseAsync` → создать сборку →
|
||||
`AddComponentAsync` → проверить → `finally CloseAsync`.
|
||||
Паттерн: построить деталь → `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. **Вставка в начало координат:** деталь-коробка 40×40×20 (эскиз 0..40 × 0..40, выдавить 20),
|
||||
сохранить, закрыть. Создать сборку, `AddComponentAsync(file, 0,0,0)`.
|
||||
- `list_components` → `Count == 1`; компонент: `SizeX≈40, SizeY≈40, SizeZ≈20` (±допуск).
|
||||
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 подтверждает работу позиции).
|
||||
- `get_bounding_box` сборки: X∈[50,90] (сдвиг origin на +50 подтверждает работу позиции).
|
||||
|
||||
Объёмом сборку тоже можно проверить (`get_part_info` на TopPart агрегирует компоненты) —
|
||||
но габарит надёжнее и прямо проверяет позицию. Артефакты `.m3d` — в gitignored `.scratch/`.
|
||||
|
||||
Reference in New Issue
Block a user