Merge feat/drawing-associative-dimension: ассоциативная привязка размеров (инкремент 7)

Флаг associate у диаметрального/радиального — привязка к спроецированной окружности
через BaseObject (значение из геометрии). 81 инструмент, 278 тестов зелёных.
This commit is contained in:
2026-05-27 22:00:16 +03:00
12 changed files with 560 additions and 75 deletions
+48 -33
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -73,7 +73,7 @@ src/Kompas.Mcp.Host/bin/Release/net8.0-windows/kompas-mcp.exe
```
src/Kompas.Mcp.Core/ COM-слой: STA-диспетчер, подключение, документы, эскизы/операции, конвертация, снимок, инспекция модели
src/Kompas.Mcp.Host/ MCP-сервер (stdio) + определения инструментов
tests/Kompas.Mcp.Tests/ 267 тестов: unit + integration (integration требуют КОМПАС)
tests/Kompas.Mcp.Tests/ 278 тестов: unit + integration (integration требуют КОМПАС)
libs/kompas-interop/ вендорские interop-сборки КОМПАС (из SDK Samples/Common)
docs/ архитектура, план, презентация, MD-база знаний SDK (Kompas3D_SDK/)
usecases/ полигон обкатки подходов (в .gitignore); приёмы поднимаются в навык kompas-3d
+6 -6
View File
File diff suppressed because one or more lines are too long
+5
View File
@@ -220,3 +220,8 @@ stateful-сессии? Пока полагаемся на последовате
`drawing_set_technical_requirements(text)` — технические требования документа (уровень документа, не вида). Паттерн API7: `(IDrawingDocument)doc.TechnicalDemand``td.Text.Str = text``td.Update()`. Перезаписывает прежние. Возвращает число строк.
Итог: **81 инструмент, 267 тестов (163 unit + 104 integration).** Нереализовано: ассоциативная привязка размеров и шероховатости к геометрии (`BaseObject`), рамка/формат листа, выноски (`Leaders`), обозначения баз (`Bases`), допуски формы (`Tolerances`).
### ✅ Ассоциативная привязка диаметрального/радиального к окружности — реализована (инкремент 7 вехи «2D-чертёж»)
**Решено (частично).** Параметр `associate=true` добавлен к `drawing_add_diametral_dimension` и `drawing_add_radial_dimension`. Реализация (API7, `DrawingService`): новый класс `CircularObjectMatch` — отбирает окружность-кандидата в `IDrawingContainer.Circles` по центру (xc,yc) + радиусу с допуском 1 мм; бросает при отсутствии или неоднозначности (концентрические окружности различаются по радиусу). Хелпер `RequireViewContainers` получает одновременно `ISymbols2DContainer` и `IDrawingContainer` для вида. `_Circle` реализует `IDrawingObject`, поэтому `dim.BaseObject = circle` без дополнительного приведения. `associate=false` — прежнее «свободное» поведение без изменений. Подтверждено спайком: `BaseObject=circle` без задания Xc/Yc/Radius → `Valid`, `NominalValue` из геометрии (цилиндр R10: Ø=20 / R=10). +5 unit-тестов (`CircularObjectMatchTests`) + 6 интеграционных (`DrawingAssociativeDimensionTests`). Итог: **81 инструмент, 278 тестов (168 unit + 110 integration).**
Нереализовано (следующие инкременты): привязка к дуге (`IDrawingContainer.Arcs`), ассоциативный угловой размер (`BaseObject1/2`, отрезки), ассоциативная шероховатость (`IRough.BaseObject`), рамка/формат листа, выноски (`Leaders`), обозначения баз (`Bases`), допуски формы (`Tolerances`).
+9 -4
View File
@@ -13,10 +13,15 @@
Готово: стандартные виды, основная надпись (штамп), размеры (линейный/диаметральный/радиальный/угловой),
текстовые обозначения (шероховатость, свободный текст, технические требования).
- **Ассоциативная привязка размеров и шероховатости к геометрии вида** (`BaseObject`/`BaseObject1/2`).
Обозначение «прилипает» к рёбрам/окружностям вида и обновляется с моделью, а не задаётся координатами.
Самый ценный следующий шаг («умный» чертёж). Требует спайка: перечисление геометрии вида
(`IDrawingContainer`: LineSegments/Circles/Arcs), выбор объекта по точке/индексу, связывание.
- **Ассоциативная привязка размеров и шероховатости к геометрии вида** (`BaseObject`/`BaseObject1/2`).
Обозначение «прилипает» к геометрии вида и обновляется с моделью, а не задаётся координатами.
- ✓ Диаметральный/радиальный → **окружность** (`Circles`): флаг `associate` (инкремент 7); поиск по
центру+радиусу (`CircularObjectMatch`), значение Ø/R из геометрии.
- ☐ Привязка к **дуге** (`Arcs`) — нужен свой спайк (`IArc` как `BaseObject`; дуги одного родителя
неотличимы по центру+радиусу — нужен ключ по углу/точке).
-**Угловой** → пара отрезков (`BaseObject1/2`, `LineSegments`).
-**Шероховатость**`IRough.BaseObject` (привязка к контуру/ребру).
- ⛔/☐ **Линейный**у `ILineDimension` нет `BaseObject`; нужен иной механизм (отложено).
-**Рамка/формат листа** (A4/A3…, ориентация) — `ILayoutSheet`/`SheetFormat`. Быстрый фундаментальный
шаг, вероятно без спайка. Делает чертёж «правильным» по ГОСТ.
-**Выноски** (`Leaders`) — `ISymbols2DContainer.Leaders` (полки-выноски с текстом).
+7 -6
View File
@@ -128,7 +128,7 @@
<!-- HERO -->
<header class="hero">
<div class="wrap">
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало+hole+hole_counterbore+hole_countersink+hole_conic+draft+переменные+assembly_add_component+assembly_add_mate+drawing_create_standard_views+drawing_fill_title_block+drawing_add_linear_dimension+drawing_add_diametral_dimension+drawing_add_radial_dimension+drawing_add_angular_dimension+drawing_add_rough+drawing_add_text+drawing_set_technical_requirements (81 инструмент)</span>
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v3+ работает — эскиз→операции→STEP→сборка→move_face→структурный осмотр→богатые эскизы→shell+rib+sweep+loft+массивы+зеркало+hole+hole_counterbore+hole_countersink+hole_conic+draft+переменные+assembly_add_component+assembly_add_mate+drawing_create_standard_views+drawing_fill_title_block+drawing_add_linear_dimension+drawing_add_diametral_dimension+drawing_add_radial_dimension+drawing_add_angular_dimension+drawing_add_rough+drawing_add_text+drawing_set_technical_requirements+associate (81 инструмент)</span>
<h1>КОМПАС-3D <span class="g">MCP-сервер</span><br>управление CAD языком LLM</h1>
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
@@ -261,7 +261,7 @@
впереди.</p>
<div class="prog">
<div class="progrow"><span>Общий прогресс</span><span>v3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell+rib+sweep+loft (пакет B) · массивы+зеркало (пакет C) · переменные (пакет D start) · offset-plane (пакет E start) · hole (API7) · hole_counterbore · hole_countersink · hole_conic · draft (API5) · assembly_add_component · assembly_add_mate · drawing_create_standard_views · drawing_fill_title_block · drawing_add_linear_dimension · drawing_add_diametral_dimension · drawing_add_radial_dimension · drawing_add_angular_dimension · drawing_add_rough · drawing_add_text · drawing_set_technical_requirements · навык kompas-3d · 81 инструмент · 267 тестов</span></div>
<div class="progrow"><span>Общий прогресс</span><span>v3+: STEP · move_face · структурный осмотр · богатые эскизы (пакет A) · shell+rib+sweep+loft (пакет B) · массивы+зеркало (пакет C) · переменные (пакет D start) · offset-plane (пакет E start) · hole (API7) · hole_counterbore · hole_countersink · hole_conic · draft (API5) · assembly_add_component · assembly_add_mate · drawing_create_standard_views · drawing_fill_title_block · drawing_add_linear_dimension · drawing_add_diametral_dimension · drawing_add_radial_dimension · drawing_add_angular_dimension · drawing_add_rough · drawing_add_text · drawing_set_technical_requirements · associate (ассоциативная привязка диам./радиального) · навык kompas-3d · 81 инструмент · 278 тестов</span></div>
<div class="bar"><i style="width:97%"></i></div>
</div>
@@ -310,8 +310,8 @@
<li class="muted"><span class="mark ok"></span><span class="t"><code>Variables</code>: <b>create_variable</b> · <b>set_variable</b> · <b>delete_variable</b> (пакет D)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Conversion</code>: import_step · export_step</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Assembly</code>: <b>assembly_add_component</b> (вставка компонента .m3d/.a3d в сборку, API7) · <b>assembly_add_mate</b> (сопряжения coincidence/distance, API7)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Drawing</code>: <b>drawing_create_standard_views</b> (стандартные ассоциативные виды чертежа, API7) · <b>drawing_fill_title_block</b> (основная надпись/штамп: обозначение, наименование, материал, API7) · <b>drawing_add_linear_dimension</b> (линейный размер на виде, ISymbols2DContainer, API7) · <b>drawing_add_diametral_dimension</b> (диаметральный размер Ø, IDiametralDimension, API7) · <b>drawing_add_radial_dimension</b> (радиальный размер R, IRadialDimension, API7) · <b>drawing_add_angular_dimension</b> (угловой размер, IAngleDimension, angleType=min/max/more, API7) · <b>drawing_add_rough</b> (знак шероховатости, IRough/IRoughParams, signType=delete/without/none, API7) · <b>drawing_add_text</b> (свободный текст на виде, IDrawingContainer/IDrawingText, API7) · <b>drawing_set_technical_requirements</b> (тех. требования документа, IDrawingDocument/TechnicalDemand, API7)</span></li>
<li><span class="mark wip"></span><span>2D-чертёж: ассоциативная привязка размеров и шероховатости к геометрии (BaseObject), рамка/формат, выноски (Leaders), обозначения баз (Bases), допуски формы (Tolerances) · пакет E (ось, угловая плоскость) · пакет D параметрика: параметрические эскизы (API2D) — <b>исследовано, недоступно через COM API</b> · прочие типы сопряжений (parallel/perpendicular/concentric/angle/tangency)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Drawing</code>: <b>drawing_create_standard_views</b> (стандартные ассоциативные виды чертежа, API7) · <b>drawing_fill_title_block</b> (основная надпись/штамп: обозначение, наименование, материал, API7) · <b>drawing_add_linear_dimension</b> (линейный размер на виде, ISymbols2DContainer, API7) · <b>drawing_add_diametral_dimension</b> (диаметральный размер Ø, IDiametralDimension, <b>associate=true</b> для ассоциативной привязки к окружности, API7) · <b>drawing_add_radial_dimension</b> (радиальный размер R, IRadialDimension, <b>associate=true</b> аналогично, API7) · <b>drawing_add_angular_dimension</b> (угловой размер, IAngleDimension, angleType=min/max/more, API7) · <b>drawing_add_rough</b> (знак шероховатости, IRough/IRoughParams, signType=delete/without/none, API7) · <b>drawing_add_text</b> (свободный текст на виде, IDrawingContainer/IDrawingText, API7) · <b>drawing_set_technical_requirements</b> (тех. требования документа, IDrawingDocument/TechnicalDemand, API7)</span></li>
<li><span class="mark wip"></span><span>2D-чертёж: привязка к дуге (Arcs), ассоциативный угловой размер, ассоциативная шероховатость (IRough.BaseObject), рамка/формат, выноски (Leaders), обозначения баз (Bases), допуски формы (Tolerances) · пакет E (ось, угловая плоскость) · пакет D параметрика: параметрические эскизы (API2D) — <b>исследовано, недоступно через COM API</b> · прочие типы сопряжений (parallel/perpendicular/concentric/angle/tangency)</span></li>
</ul>
</div>
@@ -322,7 +322,7 @@
<ul class="list">
<li class="muted"><span class="mark ok"></span><span class="t">Unit-тесты — без COM: диспетчер, enum-маппинг, StepFormat, InspectionText</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Интеграционные тесты с КОМПАС: подключение, документы, снимок, цикл, STEP round-trip, сборка, move_face, инспекция</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Итого 267 тестов зелёных (163 unit + 104 integration)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Итого 278 тестов зелёных (168 unit + 110 integration)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Сборка <code>kompas-mcp.exe</code>, README с конфигом клиента</span></li>
<li class="muted"><span class="mark ok"></span><span class="t">Навык <code>kompas-3d</code> (методика) + полигон <code>usecases/</code></span></li>
<li><span class="mark todo"></span><span>In-process тест MCP-клиента, релизный publish</span></li>
@@ -356,7 +356,8 @@
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 4 ✓</div><h4>Диаметральный размер</h4><p><b>drawing_add_diametral_dimension</b> (ISymbols2DContainer.DiametralDimensions.Add/IDiametralDimension, Xc/Yc/Radius/Angle рад, API7) · RequireSymbols2DContainer общий хелпер · DimensionAngles.ToRadians · 76 инструментов · 218 тестов</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 5 ✓</div><h4>Радиальный и угловой размеры</h4><p><b>drawing_add_radial_dimension</b> (IRadialDimension, DimensionType=true, API7) · <b>drawing_add_angular_dimension</b> (IAngleDimension, angleType=min/max/more, AngleDimensionType enum, API7) · базовое семейство размеров завершено · 78 инструментов · 241 тест</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 6 ✓</div><h4>Текстовые обозначения</h4><p><b>drawing_add_rough</b> (IRough/IRoughParams, signType=delete/without/none, API7) · <b>drawing_add_text</b> (IDrawingContainer/IDrawingText/IText, API7) · <b>drawing_set_technical_requirements</b> (IDrawingDocument/TechnicalDemand, уровень документа, API7) · DrawingAnnotationResult · RoughSignType enum · 81 инструмент · 267 тестов</p></div>
<div class="tl cur"><div class="ph">далее · в работе ▶</div><h4>Оформление чертежа</h4><p>Ассоциативная привязка размеров и шероховатости к геометрии (BaseObject) · рамка/формат · выноски (Leaders) · обозначения баз (Bases) · допуски формы (Tolerances) · пакет E (ось, угловая плоскость) · прочие типы сопряжений (parallel/perpendicular/concentric/angle/tangency) · пакет D параметрика — параметрические эскизы недоступны через COM API</p></div>
<div class="tl" style="border-color:var(--green)"><div class="ph" style="color:var(--green)">2D-чертёж · инкремент 7 ✓</div><h4>Ассоциативная привязка к окружности</h4><p><b>associate=true</b> у drawing_add_diametral_dimension и drawing_add_radial_dimension · CircularObjectMatch (поиск по центру+радиусу, допуск 1 мм) · RequireViewContainers · dim.BaseObject=circle · значение из геометрии · 81 инструмент · 278 тестов</p></div>
<div class="tl cur"><div class="ph">далее · в работе ▶</div><h4>Оформление чертежа</h4><p>Привязка к дуге (Arcs) · ассоциативный угловой размер · ассоциативная шероховатость (IRough.BaseObject) · рамка/формат · выноски (Leaders) · обозначения баз (Bases) · допуски формы (Tolerances) · пакет E (ось, угловая плоскость) · прочие типы сопряжений (parallel/perpendicular/concentric/angle/tangency) · пакет D параметрика — параметрические эскизы недоступны через COM API</p></div>
</div>
</div>
</section>
@@ -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` отсутствует (нужен иной механизм). Рамка/формат листа; выноски/базы/допуски формы.
@@ -0,0 +1,44 @@
namespace Kompas.Mcp.Core.Drawings;
/// <summary>Геометрия окружности-кандидата вида (центр + радиус, локальная СК вида, мм).</summary>
public sealed record CircleCandidate(double Xc, double Yc, double Radius);
/// <summary>
/// Чистый отбор окружности проекционного вида по ключу (центр + радиус) для ассоциативной привязки
/// размера. Выделен из COM-перечисления ради unit-тестов.
/// </summary>
public static class CircularObjectMatch
{
/// <summary>
/// Индекс единственной окружности из <paramref name="candidates"/>, чей центр и радиус совпадают с
/// ключом (<paramref name="keyX"/>,<paramref name="keyY"/>,<paramref name="keyR"/>) в пределах
/// <paramref name="tol"/> (мм): <c>dist(центр,ключ) ≤ tol</c> И <c>|R keyR| ≤ tol</c>.
/// </summary>
/// <exception cref="InvalidOperationException">Нет подходящих (0) или неоднозначно (&gt;1).</exception>
public static int SelectMatchIndex(IReadOnlyList<CircleCandidate> candidates,
double keyX, double keyY, double keyR, double tol)
{
ArgumentNullException.ThrowIfNull(candidates);
var matches = new List<int>();
for (var i = 0; i < candidates.Count; i++)
{
var c = candidates[i];
var dx = c.Xc - keyX;
var dy = c.Yc - keyY;
if (Math.Sqrt(dx * dx + dy * dy) <= tol && Math.Abs(c.Radius - keyR) <= tol)
matches.Add(i);
}
return matches.Count switch
{
1 => matches[0],
0 => throw new InvalidOperationException(
$"Окружность не найдена рядом с ({keyX},{keyY}) R≈{keyR} (допуск {tol} мм). " +
"Проверьте координаты/радиус и наличие окружности на этом виде."),
_ => throw new InvalidOperationException(
$"Неоднозначный выбор окружности у ({keyX},{keyY}) R≈{keyR}: подходит {matches.Count}. " +
"Уточните координаты/радиус."),
};
}
}
+93 -16
View File
@@ -137,27 +137,46 @@ public sealed class DrawingService
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.LineDimensions?.Count ?? 0, ct);
/// <summary>
/// Поставить диаметральный размер (Ø) окружности с центром (xc,yc) и радиусом <paramref name="radius"/>
/// (ЛОКАЛЬНАЯ СК вида, мм) на виде. <paramref name="angleDeg"/> — направление выноски (градусы).
/// Значение (диаметр = 2·radius) измеряется автоматически. Возвращает диаметр и номер вида.
/// Поставить диаметральный размер (Ø) окружности на виде. <paramref name="angleDeg"/> — направление
/// выноски (градусы). При <paramref name="associate"/>=false — свободный размер по (xc,yc,radius),
/// значение = 2·radius. При <paramref name="associate"/>=true — (xc,yc,radius) служат ключом поиска
/// спроецированной окружности (Circles), размер привязывается к ней (BaseObject), значение Ø читается
/// из геометрии. Координаты — ЛОКАЛЬНАЯ СК вида (мм). Возвращает диаметр и номер вида.
/// </summary>
public Task<DrawingDimensionResult> AddDiametralDimensionAsync(int viewNumber, double xc, double yc,
double radius, double angleDeg, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddDiametralDimensionCore(viewNumber, xc, yc, radius, angleDeg), ct);
double radius, double angleDeg, bool associate = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddDiametralDimensionCore(viewNumber, xc, yc, radius, angleDeg, associate), ct);
private DrawingDimensionResult AddDiametralDimensionCore(int viewNumber, double xc, double yc,
double radius, double angleDeg)
double radius, double angleDeg, bool associate)
{
DrawingValidation.RequireFiniteCoords(xc, yc);
DrawingValidation.RequirePositiveRadius(radius);
var angleRad = DimensionAngles.ToRadians(angleDeg);
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
// Свободный путь берёт только ISymbols2DContainer (как раньше); IDrawingContainer (геометрия)
// нужен лишь для ассоциативной привязки — не расширяем поверхность отказа свободного пути.
IView view; ISymbols2DContainer symbols; IDrawingObject? baseObject = null;
if (associate)
{
var c = RequireViewContainers(viewNumber);
view = c.View; symbols = c.Symbols;
baseObject = FindBaseCircle(c.Drawing, xc, yc, radius);
}
else
{
var c = RequireSymbols2DContainer(viewNumber);
view = c.View; symbols = c.Symbols;
}
var dim = symbols.DiametralDimensions?.Add()
?? throw new InvalidOperationException("DiametralDimensions.Add вернул null.");
try
{
dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; dim.Angle = angleRad;
if (baseObject != null)
dim.BaseObject = baseObject; // ассоциативно: значение Ø читается из геометрии
else
{ dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; }
dim.Angle = angleRad;
// DimensionType (стиль размерной линии) не задаём — оставляем COM-дефолт (полярность bool
// не подтверждена; дефолт даёт валидный размер с верным значением — проверено спайком).
((IDimensionText)dim).AutoNominalValue = true;
@@ -185,27 +204,46 @@ public sealed class DrawingService
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.DiametralDimensions?.Count ?? 0, ct);
/// <summary>
/// Поставить радиальный размер (R) окружности/дуги с центром (xc,yc) и радиусом <paramref name="radius"/>
/// (ЛОКАЛЬНАЯ СК вида, мм) на виде. <paramref name="angleDeg"/> — направление выноски (градусы).
/// Значение (= радиус) измеряется автоматически. Возвращает радиус и номер вида.
/// Поставить радиальный размер (R) окружности на виде. <paramref name="angleDeg"/> — направление
/// выноски (градусы). При <paramref name="associate"/>=false — свободный размер по (xc,yc,radius),
/// значение = radius. При <paramref name="associate"/>=true — (xc,yc,radius) служат ключом поиска
/// спроецированной окружности (Circles), размер привязывается к ней (BaseObject), значение R читается
/// из геометрии. Координаты — ЛОКАЛЬНАЯ СК вида (мм). Возвращает радиус и номер вида.
/// </summary>
public Task<DrawingDimensionResult> AddRadialDimensionAsync(int viewNumber, double xc, double yc,
double radius, double angleDeg, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddRadialDimensionCore(viewNumber, xc, yc, radius, angleDeg), ct);
double radius, double angleDeg, bool associate = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddRadialDimensionCore(viewNumber, xc, yc, radius, angleDeg, associate), ct);
private DrawingDimensionResult AddRadialDimensionCore(int viewNumber, double xc, double yc,
double radius, double angleDeg)
double radius, double angleDeg, bool associate)
{
DrawingValidation.RequireFiniteCoords(xc, yc);
DrawingValidation.RequirePositiveRadius(radius);
var angleRad = DimensionAngles.ToRadians(angleDeg);
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
// Свободный путь берёт только ISymbols2DContainer (как раньше); IDrawingContainer (геометрия)
// нужен лишь для ассоциативной привязки — не расширяем поверхность отказа свободного пути.
IView view; ISymbols2DContainer symbols; IDrawingObject? baseObject = null;
if (associate)
{
var c = RequireViewContainers(viewNumber);
view = c.View; symbols = c.Symbols;
baseObject = FindBaseCircle(c.Drawing, xc, yc, radius);
}
else
{
var c = RequireSymbols2DContainer(viewNumber);
view = c.View; symbols = c.Symbols;
}
var dim = symbols.RadialDimensions?.Add()
?? throw new InvalidOperationException("RadialDimensions.Add вернул null.");
try
{
dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; dim.Angle = angleRad;
if (baseObject != null)
dim.BaseObject = baseObject; // ассоциативно: значение R читается из геометрии
else
{ dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; }
dim.Angle = angleRad;
dim.DimensionType = true; // стиль R «от центра» (на величину не влияет — спайк)
((IDimensionText)dim).AutoNominalValue = true;
@@ -429,6 +467,45 @@ public sealed class DrawingService
return (view, container);
}
/// <summary>Допуск совпадения окружности с ключом поиска при ассоциативной привязке (мм).</summary>
private const double CircleMatchToleranceMm = 1.0;
/// <summary>Вид + оба контейнера (QI от одного вида): ISymbols2DContainer (создание размера) и
/// IDrawingContainer (поиск геометрии для ассоциативной привязки).</summary>
private (IView View, ISymbols2DContainer Symbols, IDrawingContainer Drawing) RequireViewContainers(int viewNumber)
{
var view = FindView(viewNumber);
var symbols = view as ISymbols2DContainer
?? throw new InvalidOperationException("Вид не приводится к ISymbols2DContainer.");
var drawing = view as IDrawingContainer
?? throw new InvalidOperationException("Вид не приводится к IDrawingContainer.");
return (view, symbols, drawing);
}
/// <summary>Найти окружность вида по ключу (центр + радиус) и вернуть её как IDrawingObject
/// (для <c>dim.BaseObject</c>). Бросает при отсутствии/неоднозначности.</summary>
private static IDrawingObject FindBaseCircle(IDrawingContainer drawing, double xc, double yc, double radius)
{
var items = CollectCircles(drawing);
var index = CircularObjectMatch.SelectMatchIndex(
items.Select(i => i.Geom).ToList(), xc, yc, radius, CircleMatchToleranceMm);
return items[index].Obj;
}
/// <summary>Собрать окружности вида (IDrawingContainer.Circles) с их геометрией и COM-ссылкой.</summary>
private static List<(IDrawingObject Obj, CircleCandidate Geom)> CollectCircles(IDrawingContainer drawing)
{
var result = new List<(IDrawingObject, CircleCandidate)>();
var circles = drawing.Circles;
if (circles == null) return result;
for (var i = 0; i < circles.Count; i++)
{
if (circles[i] is not ICircle c) continue;
result.Add(((IDrawingObject)c, new CircleCandidate(c.Xc, c.Yc, c.Radius)));
}
return result;
}
/// <summary>Вид + его ISymbols2DContainer (QI от вида). Общий для линейных/диаметральных/… размеров.</summary>
private (IView View, ISymbols2DContainer Symbols) RequireSymbols2DContainer(int viewNumber)
{
+19 -9
View File
@@ -71,35 +71,45 @@ public sealed class DrawingTools(KompasSession session, DrawingService drawing)
"задаётся центром (xc,yc) и радиусом radius в ЛОКАЛЬНОЙ СК вида (мм; при масштабе 1:1 = координаты " +
"модели на плоскости вида) — задайте параметры реальной окружности вида. angle — направление выноски " +
"в градусах. viewNumber — номер вида из drawing_create_standard_views (0 = первый/главный). " +
"Значение (диаметр = 2·radius) измеряется автоматически. Возвращает диаметр и номер вида.")]
"associate=false: свободный размер (значение = 2·radius). associate=true: АССОЦИАТИВНО — (xc,yc,radius) " +
"используются как ключ для поиска спроецированной окружности на виде; размер привязывается к ней " +
"(BaseObject) и значение Ø читается из геометрии и обновляется с моделью. Значение измеряется " +
"автоматически. Возвращает диаметр и номер вида.")]
public async Task<string> AddDiametralDimension(
[Description("X центра окружности (СК вида, мм)")] double xc,
[Description("Y центра окружности")] double yc,
[Description("Радиус окружности, мм")] double radius,
[Description("Направление выноски, градусы")] double angle = 0,
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0)
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0,
[Description("true = привязать к окружности вида (значение из геометрии); false = свободный размер")] bool associate = false)
{
await session.ConnectAsync();
var r = await drawing.AddDiametralDimensionAsync(viewNumber, xc, yc, radius, angle);
return $"Диаметральный размер поставлен на виде {r.ViewNumber}, Ø{r.Value:0.###} мм.";
var r = await drawing.AddDiametralDimensionAsync(viewNumber, xc, yc, radius, angle, associate);
var mode = associate ? " (ассоциативно)" : "";
return $"Диаметральный размер поставлен на виде {r.ViewNumber}, Ø{r.Value:0.###} мм{mode}.";
}
[McpServerTool(Name = "drawing_add_radial_dimension")]
[Description("Поставить радиальный размер (R) окружности/дуги на виде активного чертежа. Окружность " +
[Description("Поставить радиальный размер (R) окружности на виде активного чертежа. Окружность " +
"задаётся центром (xc,yc) и радиусом radius в ЛОКАЛЬНОЙ СК вида (мм; при масштабе 1:1 = координаты " +
"модели на плоскости вида) — задайте параметры реальной окружности вида. angle — направление выноски " +
"в градусах. viewNumber — номер вида из drawing_create_standard_views (0 = первый/главный). " +
"Значение (= радиус) измеряется автоматически. Возвращает радиус и номер вида.")]
"associate=false: свободный размер (значение = radius). associate=true: АССОЦИАТИВНО — (xc,yc,radius) " +
"используются как ключ для поиска спроецированной окружности на виде; размер привязывается к ней " +
"(BaseObject) и значение R читается из геометрии и обновляется с моделью. Значение измеряется " +
"автоматически. Возвращает радиус и номер вида.")]
public async Task<string> AddRadialDimension(
[Description("X центра окружности (СК вида, мм)")] double xc,
[Description("Y центра окружности")] double yc,
[Description("Радиус окружности, мм")] double radius,
[Description("Направление выноски, градусы")] double angle = 0,
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0)
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0,
[Description("true = привязать к окружности вида (значение из геометрии); false = свободный размер")] bool associate = false)
{
await session.ConnectAsync();
var r = await drawing.AddRadialDimensionAsync(viewNumber, xc, yc, radius, angle);
return $"Радиальный размер поставлен на виде {r.ViewNumber}, R{r.Value:0.###} мм.";
var r = await drawing.AddRadialDimensionAsync(viewNumber, xc, yc, radius, angle, associate);
var mode = associate ? " (ассоциативно)" : "";
return $"Радиальный размер поставлен на виде {r.ViewNumber}, R{r.Value:0.###} мм{mode}.";
}
[McpServerTool(Name = "drawing_add_angular_dimension")]
@@ -0,0 +1,53 @@
using Kompas.Mcp.Core.Drawings;
namespace Kompas.Mcp.Tests;
[Trait("Category", "Unit")]
public sealed class CircularObjectMatchTests
{
private const double Tol = 1.0;
private static IReadOnlyList<CircleCandidate> Candidates(params (double x, double y, double r)[] items)
=> items.Select(i => new CircleCandidate(i.x, i.y, i.r)).ToList();
[Fact]
public void SelectMatchIndex_returns_unique_match()
{
var c = Candidates((0, 0, 10), (50, 20, 5));
// ключ с допустимым отклонением центра и радиуса
Assert.Equal(0, CircularObjectMatch.SelectMatchIndex(c, 0.3, -0.2, 10.3, Tol));
Assert.Equal(1, CircularObjectMatch.SelectMatchIndex(c, 50, 20, 5, Tol));
}
[Fact]
public void SelectMatchIndex_concentric_disambiguated_by_radius()
{
// концентрические R5 и R10 (цековка): ключ R10 выбирает внешнюю, не неоднозначно
var c = Candidates((0, 0, 5), (0, 0, 10));
Assert.Equal(1, CircularObjectMatch.SelectMatchIndex(c, 0, 0, 10, Tol));
Assert.Equal(0, CircularObjectMatch.SelectMatchIndex(c, 0, 0, 5, Tol));
}
[Fact]
public void SelectMatchIndex_throws_when_none_in_tolerance()
{
var c = Candidates((0, 0, 10));
// центр далеко
Assert.Throws<InvalidOperationException>(() => CircularObjectMatch.SelectMatchIndex(c, 100, 100, 10, Tol));
// радиус далеко
Assert.Throws<InvalidOperationException>(() => CircularObjectMatch.SelectMatchIndex(c, 0, 0, 25, Tol));
}
[Fact]
public void SelectMatchIndex_throws_when_ambiguous()
{
// две почти совпадающие окружности в допуске
var c = Candidates((0, 0, 10), (0.2, 0.2, 10.2));
Assert.Throws<InvalidOperationException>(() => CircularObjectMatch.SelectMatchIndex(c, 0, 0, 10, Tol));
}
[Fact]
public void SelectMatchIndex_throws_on_empty()
=> Assert.Throws<InvalidOperationException>(
() => CircularObjectMatch.SelectMatchIndex(Candidates(), 0, 0, 10, Tol));
}
@@ -0,0 +1,143 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Drawings;
using Kompas.Mcp.Core.Modeling;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: ассоциативная привязка диаметрального/радиального размера к окружности вида
/// (BaseObject). Модель — цилиндр R10 (вид проецирует окружность).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class DrawingAssociativeDimensionTests : IntegrationTestBase
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly DrawingService _drawing;
public DrawingAssociativeDimensionTests(KompasFixture fx) : base(fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
_drawing = new DrawingService(fx.Session, fx.Dispatcher);
}
private async Task<string> BuildAndSaveCylinderAsync()
{
var path = TestPaths.NewFile(".m3d");
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(s, 0, 0, 10);
await _modeler.CloseSketchAsync(s);
await _modeler.ExtrudeAsync(s, depth: 20);
await _modeler.RebuildAsync();
await _docs.SaveAsAsync(path);
}
finally { await _docs.CloseAsync(save: false); }
return path;
}
[Fact]
public async Task Diametral_associative_reads_diameter_from_geometry()
{
var partFile = await BuildAndSaveCylinderAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var target = views.ViewNumbers[0]; // вид с окружностью (спайк)
var before = await _drawing.GetViewDiametralDimensionCountAsync(target);
// Ключ radius=10.3 (в допуске 1.0 находит окружность R10). Ассоциативно → Ø=20 из геометрии,
// а НЕ 2·10.3=20.6 → доказывает чтение значения из геометрии, а не из входа.
var r = await _drawing.AddDiametralDimensionAsync(target, xc: 0, yc: 0, radius: 10.3,
angleDeg: 45, associate: true);
var after = await _drawing.GetViewDiametralDimensionCountAsync(target);
Assert.Equal(target, r.ViewNumber);
Assert.InRange(r.Value, 19.9, 20.1); // Ø=20 из геометрии (не 20.6)
Assert.Equal(before + 1, after);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Radial_associative_reads_radius_from_geometry()
{
var partFile = await BuildAndSaveCylinderAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var target = views.ViewNumbers[0];
var r = await _drawing.AddRadialDimensionAsync(target, xc: 0, yc: 0, radius: 10.3,
angleDeg: -45, associate: true);
Assert.InRange(r.Value, 9.9, 10.1); // R=10 из геометрии (не 10.3)
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Associative_no_circle_near_throws_and_no_orphan()
{
var partFile = await BuildAndSaveCylinderAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var target = views.ViewNumbers[0];
var before = await _drawing.GetViewDiametralDimensionCountAsync(target);
// Ключ далеко от любой окружности → отбор бросит до Add (размер не создаётся).
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddDiametralDimensionAsync(target, xc: 1000, yc: 1000, radius: 5,
angleDeg: 0, associate: true));
var after = await _drawing.GetViewDiametralDimensionCountAsync(target);
Assert.Equal(before, after); // нет «осиротевшего» размера
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Free_diametral_path_unchanged()
{
var partFile = await BuildAndSaveCylinderAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
// associate=false (по умолчанию) — свободный размер: значение = 2·radius.
var r = await _drawing.AddDiametralDimensionAsync(views.ViewNumbers[0], xc: 0, yc: 0,
radius: 15, angleDeg: 0, associate: false);
Assert.InRange(r.Value, 29.9, 30.1);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Free_radial_path_unchanged()
{
var partFile = await BuildAndSaveCylinderAsync();
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
var r = await _drawing.AddRadialDimensionAsync(views.ViewNumbers[0], xc: 0, yc: 0,
radius: 12, angleDeg: 0, associate: false);
Assert.InRange(r.Value, 11.9, 12.1);
}
finally { await _docs.CloseAsync(save: false); }
}
[Fact]
public async Task Associative_no_views_throws()
{
await _docs.CreateAsync(KompasDocumentType.Drawing);
try
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
_drawing.AddDiametralDimensionAsync(0, xc: 0, yc: 0, radius: 10, angleDeg: 0, associate: true));
}
finally { await _docs.CloseAsync(save: false); }
}
}