feat(drawing): ассоциативная привязка диаметрального/радиального размера к окружности вида
Флаг associate у drawing_add_diametral_dimension/drawing_add_radial_dimension: - associate=false (по умолчанию) — прежний свободный размер (без изменений); - associate=true — (xc,yc,radius) как ключ поиска спроецированной окружности (IDrawingContainer.Circles), привязка через BaseObject; значение Ø/R читается из геометрии и обновляется с моделью. - CircularObjectMatch.SelectMatchIndex (чистый отбор по центру+радиусу, допуск 1 мм; бросает при отсутствии/неоднозначности; концентрические разделяются радиусом) - RequireViewContainers (вид → ISymbols2DContainer + IDrawingContainer), CollectCircles, FindBaseCircle - 5 unit + 6 интеграционных тестов (всего 278 зелёных), сборка Release чистая Спайк подтвердил: BaseObject=circle без задания Xc/Yc/Radius → Valid, NominalValue из геометрии (Ø=20/R=10 для R10). Объём — только окружности (Circles); дуги/угловой/линейный — далее. Ревью Codex спека учтено (оба контейнера, guard вокруг блока, доказательство ассоциативности тестом radius=10.3→Ø20, регресс обоих свободных путей). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
# Дизайн: ассоциативная привязка диаметрального и радиального размеров к геометрии вида (API7)
|
||||
|
||||
**Дата:** 2026-05-27
|
||||
**Статус:** дизайн согласован (вариант A — флаг `associate`), спайк проведён, ревью Codex спека учтено — к реализации
|
||||
|
||||
## Правки по ревью Codex (спек)
|
||||
|
||||
- **#3/#4** объём сужен до **окружностей** (`IDrawingContainer.Circles`): спайк подтвердил привязку
|
||||
только к окружности. Дуги (`Arcs`) — следующий инкремент со своим спайком (привязка `IArc` как
|
||||
`BaseObject` для диам./радиального не проверена; дуги одного родителя неотличимы по центру+радиусу).
|
||||
- **#1** вид резолвится один раз и приводится к обоим контейнерам: `ISymbols2DContainer` (создание
|
||||
размера) И `IDrawingContainer` (поиск окружности). Новый helper `RequireViewContainers(viewNumber)`
|
||||
→ `(IView, ISymbols2DContainer, IDrawingContainer)`.
|
||||
- **#2** весь блок после `…Dimensions.Add()` (присвоение `BaseObject`/`Angle`/`AutoNominalValue`,
|
||||
`Update`, `Valid`, read-back) — внутри того же `try/catch` с `Delete` при любом сбое (как свободный путь).
|
||||
- **#5** тест доказывает ассоциативность: ключ поиска `radius=10.3` (в допуске находит окружность R10)
|
||||
→ associate вернёт Ø=20 (из геометрии), свободный дал бы 2·10.3=20.6; `Value≈20` (не 20.6) доказывает
|
||||
чтение из геометрии. + диагностический read-back `BaseObject != null`.
|
||||
- **#6** обе ветки (свободная/associate) конвертируют `angle` через `DimensionAngles.ToRadians(angleDeg)`
|
||||
перед присвоением `dim.Angle`.
|
||||
- **#7** пост-инвариант значения (`value > 0 && double.IsFinite`) сохраняется и в associate-ветке (иначе откат).
|
||||
- **#8** регресс-тесты свободного пути (`associate=false`) для ОБОИХ — диаметрального и радиального.
|
||||
|
||||
## Цель
|
||||
|
||||
Веха 2D-ЧЕРТЁЖ, инкремент 7. Размеры пока «свободные» (значение из заданных координат). Ассоциативная
|
||||
привязка делает диаметральный/радиальный размер **связанным с проекцией геометрии**: значение (Ø/R)
|
||||
читается из спроецированной окружности/дуги вида через `BaseObject` и обновляется вместе с моделью.
|
||||
Первый шаг к «умному» чертежу.
|
||||
|
||||
## Объём (обоснован рефлексией interop)
|
||||
|
||||
Только **диаметральный** и **радиальный**, привязка только к **окружности** (`IDrawingContainer.Circles`)
|
||||
— у `IDiametralDimension`/`IRadialDimension` есть свойство `BaseObject`; `_Circle` реализует
|
||||
`IDrawingObject`, поэтому объект из коллекции присваивается `dim.BaseObject` (подтверждено спайком).
|
||||
У `ILineDimension` `BaseObject` НЕТ; угловой требует `BaseObject1/2`; привязка к дуге (`Arcs`) не
|
||||
проверена — всё это отдельные инкременты (вне объёма).
|
||||
|
||||
## Спайк: механизм подтверждён вживую (НЕ разучивать)
|
||||
|
||||
Спайк (`_SpikeAssocDim`, прогнан на реальном КОМПАС, затем удалён): цилиндр (окружность R10, выдавлен
|
||||
на 20) → стандартные виды. Один из видов содержит спроецированную окружность.
|
||||
|
||||
```
|
||||
IDrawingContainer dc = (IDrawingContainer)view; // QI от вида (как ISymbols2DContainer)
|
||||
ICircles circles = dc.Circles; // Count=1; circles[0] as ICircle → Xc=0,Yc=0,R=10
|
||||
IDiametralDimension dia = symbols.DiametralDimensions.Add();
|
||||
dia.BaseObject = circles[0]; // IDrawingObject; Xc/Yc/Radius НЕ задаём
|
||||
dia.Angle = π/4; // направление выноски (размещение)
|
||||
dia.Update(); // True, Valid=True
|
||||
double v = ((IDimensionText)dia).NominalValue; // == 20 — ДИАМЕТР, прочитан из геометрии!
|
||||
```
|
||||
|
||||
**Проверено:** `BaseObject=circle` без задания координат → `Update=True`, `Valid=True`,
|
||||
диаметральный `NominalValue=20` (=2·R геометрии), радиальный `NominalValue=10` (=R). `BaseObject`
|
||||
остаётся непустым после. Геометрия вида: `IDrawingContainer.Circles` (`ICircle`: `Xc/Yc/Radius`),
|
||||
`.Arcs` (`IArc`: `Xc/Yc/Radius/Angle1/Angle2`), `.LineSegments`. **Нет** «find by point» — выбор
|
||||
перебором коллекции.
|
||||
|
||||
## MCP-инструменты (расширение существующих, вариант A)
|
||||
|
||||
К `drawing_add_diametral_dimension` и `drawing_add_radial_dimension` добавляется параметр
|
||||
`associate=false`:
|
||||
|
||||
| `associate` | Поведение |
|
||||
|---|---|
|
||||
| `false` (по умолчанию) | Текущее «свободное»: `Xc/Yc/Radius` задаются напрямую (без изменений, обратная совместимость). |
|
||||
| `true` | `(xc,yc,radius)` — **ключ поиска**: найти в виде окружность/дугу с центром ≈ (xc,yc) и радиусом ≈ radius, привязать `BaseObject`; значение (Ø/R) читается из геометрии. |
|
||||
|
||||
Координаты ключа — view-local (= координаты модели при масштабе 1:1; при ином масштабе — масштабированные,
|
||||
как у прочих размеров). `angle` — направление выноски (°), как сейчас. `viewNumber` — как сейчас.
|
||||
|
||||
## Выбор объекта
|
||||
|
||||
В `IDrawingContainer` нет «find by point». Перебираем `Circles`, собираем кандидатов `(Xc, Yc, Radius)`,
|
||||
ищем подходящий по ключу `(xc, yc, radius)`:
|
||||
- кандидат подходит, если `dist(центр, ключ) ≤ tol` И `|R − radius| ≤ tol` (`tol = 1.0 мм`, константа);
|
||||
- ровно 1 подходящий → привязываем; 0 → ошибка «окружность не найдена рядом с (xc,yc) R≈radius»;
|
||||
>1 → ошибка «неоднозначно — уточните координаты/радиус».
|
||||
|
||||
Радиус в ключе снимает неоднозначность концентрических окружностей (цековка R5/R10 — ключ R10
|
||||
выберет только внешнюю). Логика отбора — чистая (unit-тест), перечисление COM — в сервисе.
|
||||
|
||||
## Архитектура
|
||||
|
||||
- `DrawingService.AddDiametralDimensionAsync(viewNumber, xc, yc, radius, angleDeg, associate, ct)` и
|
||||
`AddRadialDimensionAsync(..., associate, ct)` — добавлен `associate` (по умолчанию `false`). При `false` —
|
||||
текущая ветка без изменений. Обе ветки: `angleRad = DimensionAngles.ToRadians(angleDeg)`.
|
||||
- При `associate=true`: `RequireViewContainers(viewNumber)` → собрать кандидатов из `Circles`
|
||||
(`CollectCircles`) → `CircularObjectMatch.SelectMatchIndex(...)` → взять `IDrawingObject` по индексу →
|
||||
`symbols.{Diametral|Radial}Dimensions.Add()` → **внутри `try/catch`+`Delete`**: `dim.BaseObject = obj`,
|
||||
`dim.Angle = angleRad`, `AutoNominalValue=true` → `Update()` (FALSE→откат) → `Valid` (false→откат) →
|
||||
`value = NominalValue` (из геометрии); `value<=0 || !IsFinite` → откат → `DrawingDimensionResult`.
|
||||
- Новый helper `RequireViewContainers(viewNumber)` → `(IView View, ISymbols2DContainer Symbols,
|
||||
IDrawingContainer Drawing)` — резолвит вид один раз, оба QI с раздельной диагностикой (переиспользует
|
||||
`FindView`).
|
||||
- Новый файл `src/Kompas.Mcp.Core/Drawings/CircularObjectMatch.cs`: чистый статический
|
||||
`record CircleCandidate(double Xc, double Yc, double Radius)` + `int SelectMatchIndex(IReadOnlyList<CircleCandidate>
|
||||
candidates, double keyX, double keyY, double keyR, double tol)` — возвращает индекс единственного
|
||||
совпадения; `InvalidOperationException` при 0 (нет) / >1 (неоднозначно).
|
||||
- Сбор кандидатов из COM (Circles в список с сохранением ссылки на `IDrawingObject` + геометрии) — в
|
||||
сервисе (private helper `CollectCircles(IDrawingContainer)` → `List<(IDrawingObject Obj, CircleCandidate Geom)>`).
|
||||
- `DrawingDimensionResult` переиспользуется (значение из геометрии). Свободный путь не трогаем.
|
||||
|
||||
## Валидация
|
||||
|
||||
- Оба пути: `RequireFiniteCoords(xc, yc, angleDeg)` + `RequirePositiveRadius(radius)` (есть). В
|
||||
associate-режиме radius — положительный ключ поиска.
|
||||
- `CircularObjectMatch.SelectMatchIndex` — чистая логика отбора (unit).
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Unit (`CircularObjectMatchTests`)
|
||||
- Единственное совпадение по центру+радиусу → индекс.
|
||||
- Нет кандидатов в допуске → `InvalidOperationException` (нет).
|
||||
- Два совпадения в допуске → `InvalidOperationException` (неоднозначно).
|
||||
- Концентрические (один центр, разный R): ключ по R выбирает нужный (не неоднозначно).
|
||||
- Пустой список → `InvalidOperationException`.
|
||||
|
||||
### Integration (`DrawingAssociativeDimensionTests`; модель — цилиндр R10, как в спайке)
|
||||
1. **Диаметральный associative + доказательство геометрии**: ключ `(0,0,10.3)` (radius в допуске
|
||||
находит окружность R10) → `Value ≈ 20` (Ø из геометрии, НЕ 2·10.3=20.6 — доказывает чтение из
|
||||
геометрии); попадание в вид (`DiametralDimensions.Count` +1).
|
||||
2. **Радиальный associative**: `(0,0,10.3)` → `Value ≈ 10` (R из геометрии, не 10.3).
|
||||
3. **Нет окружности рядом**: ключ далеко от геометрии → `InvalidOperationException`; счётчик не вырос (откат).
|
||||
4. **Свободный путь не сломан** — диаметральный `associate=false` `(xc,yc,15)` → `Value ≈ 30`.
|
||||
5. **Свободный путь не сломан** — радиальный `associate=false` `(xc,yc,12)` → `Value ≈ 12` (регресс обоих).
|
||||
6. **Нет видов** (associate) → понятная ошибка.
|
||||
|
||||
## Дальнейшее (вне спека)
|
||||
- Ассоциативная привязка углового (`BaseObject1/2` — пара отрезков) и шероховатости (`IRough.BaseObject`);
|
||||
линейный — `BaseObject` отсутствует (нужен иной механизм). Рамка/формат листа; выноски/базы/допуски формы.
|
||||
Reference in New Issue
Block a user