From 5f17884f069dac87e56c998c380398eedf2d944a Mon Sep 17 00:00:00 2001 From: Shahovalov MIkhail Date: Wed, 27 May 2026 12:57:04 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=81=D0=BF=D0=B5=D0=BA=20assembly=5Fa?= =?UTF-8?q?dd=5Fcomponent=20=E2=80=94=20=D1=83=D1=87=D1=82=D0=B5=D0=BD?= =?UTF-8?q?=D1=8B=208=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=87=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B5=D0=B2=D1=8C=D1=8E=20Codex?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверки COM-результатов (AddFromFile/SetOrigin/UpdatePlacement), helper RequireActiveAssembly (раздельные ошибки, проверка типа документа до TopPart), валидация расширения .m3d/.a3d + нормализация пути, before/after count в тесте, фиксированная геометрия детали от нуля, проверка сохранённости файла. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...026-05-27-assembly-add-component-design.md | 71 +++++++++++++------ 1 file changed, 51 insertions(+), 20 deletions(-) diff --git a/docs/superpowers/specs/2026-05-27-assembly-add-component-design.md b/docs/superpowers/specs/2026-05-27-assembly-add-component-design.md index 321c4b4..58d3121 100644 --- a/docs/superpowers/specs/2026-05-27-assembly-add-component-design.md +++ b/docs/superpowers/specs/2026-05-27-assembly-add-component-design.md @@ -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/`.