Compare commits
3 Commits
ecabaa1828
...
ecf2e22265
| Author | SHA1 | Date | |
|---|---|---|---|
| ecf2e22265 | |||
| d3b3a7d589 | |||
| 775128484e |
@@ -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) или неоднозначно (>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}. " +
|
||||
"Уточните координаты/радиус."),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user