feat(drawing): радиальный (R) и угловой размеры на виде чертежа
drawing_add_radial_dimension (R окружности/дуги; NominalValue = радиус) и drawing_add_angular_dimension (угол между двумя сторонами; angleType min/max/more выбирает измеряемый сектор) через API7 ISymbols2DContainer. Завершают базовое семейство размеров чертежа (после линейного и диаметрального). - DrawingService: AddRadialDimensionAsync/AddAngularDimensionAsync + счётчики - AngleDimensionType (enum + Parse/ToKompas, по образцу DimensionOrientations) - DrawingDimensionResult.Value — единице-нейтральный (мм / градусы) - 9 unit + 13 интеграционных тестов (всего 240 зелёных) Спайк подтвердил: радиальный NominalValue = РАДИУС (не диаметр, вопреки справке); у углового измеряемый угол выбирает angleType (min=90°/more=270°), а (x3,y3) лишь позиционирует дугу. Спек и ревью Codex — docs/superpowers/specs/. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
# Дизайн: радиальный (R) и угловой размеры на виде чертежа через API7
|
||||
|
||||
**Дата:** 2026-05-27
|
||||
**Статус:** реализовано и проверено (5 радиальных + 8 угловых интеграционных + 9 unit; всего 240 тестов зелёных); ревью Codex спека учтено
|
||||
|
||||
## Уточнение по реализации (важно)
|
||||
|
||||
Спайк изначально решил, что измеряемый сектор выбирает положение дуги `(x3,y3)`. **При реализации
|
||||
выяснилось иначе:** сектор (90° vs 270°) выбирает **`angleType`** (`ksADMinAngle`/`MaxAngle`/`MoreAngle`),
|
||||
а `(x3,y3)` лишь позиционирует размерную дугу и задаёт её радиус. (270° в спайке возникал потому, что
|
||||
`X3/Y3` не были заданы = совпадали с вершиной → вырождение.) Проверено: `angleType=Min`→90°,
|
||||
`angleType=More`→270° на одной геометрии. Тест #8 и описания инструмента/сервиса исправлены под это.
|
||||
|
||||
## Правки по ревью Codex (спек)
|
||||
|
||||
- **#1** угловой: добавлен `RequireDistinctPoints(x1,y1,x2,y2)` (совпавшие точки сторон → одинаковые
|
||||
лучи → вырождение ещё до `Add`).
|
||||
- **#3** добавлен тест отката после `Add`: коллинеарные сонаправленные лучи (вершина(0,0),(10,0),(20,0))
|
||||
→ угол ≈0 → пост-проверка `value<=0` → откат `Delete` + ошибка; счётчик угловых не изменился.
|
||||
- **#2** поведение `x3,y3` (выбор сектора) задокументировано в описании инструмента; добавлен
|
||||
интеграционный тест выбора сектора (та же геометрия, `x3,y3` в дополнительном секторе → ~270°).
|
||||
Большой/тупой угол НЕ отвергаем — это легитимный результат (рефлексный угол через `more`/положение дуги).
|
||||
- **#4** добавлены тесты на несуществующий `viewNumber>0` (ветка `FindView` «вид не найден») для обоих.
|
||||
- **#5** `DrawingDimensionResult.Value` — XML-док станет единице-нейтральным («значение размера»);
|
||||
ответ `drawing_add_angular_dimension` форматируется в градусах (°), не «мм».
|
||||
- **#6** радиальный использует `DimensionAngles.ToRadians(angleDeg)` (как диаметральный), не ручное `·π/180`.
|
||||
|
||||
## Цель
|
||||
|
||||
Веха 2D-ЧЕРТЁЖ, инкремент 5. После линейного и диаметрального размеров — **радиальный (R)** и
|
||||
**угловой** размеры на виде. Завершают базовое семейство размеров чертежа. Оба переиспользуют
|
||||
проверенный путь `ISymbols2DContainer` (QI от `IView`) и паттерн «Add → set → Update → проверка
|
||||
`Valid` → чтение `NominalValue` → откат `Delete` при ошибке».
|
||||
|
||||
## Спайк: поведение подтверждено вживую (НЕ разучивать)
|
||||
|
||||
Спайк (`_SpikeDimensions`, прогнан на реальном КОМПАС, затем удалён) на коробке 40×30×20 с
|
||||
тремя стандартными видами. Контейнер размеров — `(ISymbols2DContainer)view`.
|
||||
|
||||
### Радиальный
|
||||
|
||||
```
|
||||
IRadialDimension rd = symbols.RadialDimensions.Add();
|
||||
rd.Xc = 10; rd.Yc = 10; rd.Radius = 15; rd.Angle = π/4; // Angle — РАДИАНЫ (направление выноски)
|
||||
rd.DimensionType = true; // BOOL: на значение НЕ влияет (стиль)
|
||||
((IDimensionText)rd).AutoNominalValue = true;
|
||||
rd.Update(); // True, rd.Valid == True
|
||||
double value = ((IDimensionText)rd).NominalValue; // == 15 — РАДИУС (не диаметр!)
|
||||
```
|
||||
|
||||
**Ключевой факт (расходится со справкой SDK):** у радиального `NominalValue` возвращает **радиус**
|
||||
(`Radius=15 → NominalValue=15`), а НЕ диаметр. Проверено для `DimensionType` и `true`, и `false` —
|
||||
оба `Valid`, значение одинаковое (BOOL влияет лишь на стиль «от центра / нет», не на величину).
|
||||
Координаты — локальная СК вида, мм. Размер «свободный» (по `Xc/Yc/Radius`, без `BaseObject`).
|
||||
|
||||
### Угловой
|
||||
|
||||
```
|
||||
IAngleDimension ad = symbols.AngleDimensions.Add(DrawingObjectTypeEnum.ksDrADimension); // =10
|
||||
ad.Xc = 0; ad.Yc = 0; // вершина угла
|
||||
ad.X1 = 10; ad.Y1 = 0; // точка на стороне 1 → луч (Xc,Yc)→(X1,Y1)
|
||||
ad.X2 = 0; ad.Y2 = 10; // точка на стороне 2 → луч (Xc,Yc)→(X2,Y2)
|
||||
ad.X3 = 5; ad.Y3 = 5; // положение размерной дуги — ЗАДАЁТ ИЗМЕРЯЕМЫЙ СЕКТОР + радиус дуги
|
||||
ad.DimensionType = ksADMinAngle; // 0=min / 1=max / 2=more180
|
||||
((IDimensionText)ad).AutoNominalValue = true;
|
||||
ad.Update(); // True, ad.Valid == True
|
||||
double value = ((IDimensionText)ad).NominalValue; // == 90 — ГРАДУСЫ
|
||||
```
|
||||
|
||||
**Проверено:**
|
||||
- вершина(0,0), точки (10,0)&(0,10), `X3/Y3`=(5,5) → **90°** ✓;
|
||||
- вершина(0,0), точки (10,0)&(10,10), `X3/Y3`=(8,4) → **45°** ✓.
|
||||
- **`X3/Y3` критичен:** без него тот же угол измерился как **270°** (дополнительный сектор). Точка
|
||||
положения дуги задаёт, какой из двух секторов меряется; заодно радиус дуги пересчитывается в
|
||||
`dist(вершина, X3/Y3)` (заданный `Radius` перетирается → его не экспонируем).
|
||||
- Подход через `Angle1/Angle2` (углы наклона сторон) — **вырожденный** (точки схлопываются к
|
||||
вершине, `X1≈0.001`), не используем. Геометрию ведут только координаты точек.
|
||||
- `NominalValue` — в **градусах**.
|
||||
|
||||
`Add(DrawingObjectTypeEnum)` требует код типа: `ksDrADimension = 10` (обычный угловой),
|
||||
`ksDrABreakDimension = 39` (с обрывом — вне объёма).
|
||||
|
||||
## MCP-инструменты
|
||||
|
||||
| Инструмент | Параметры | Поведение |
|
||||
|---|---|---|
|
||||
| `drawing_add_radial_dimension` | `xc,yc` (центр окружности), `radius`, `angle=0` (°), `viewNumber=0` | Радиальный размер (R) окружности/дуги с центром (xc,yc) и радиусом `radius` (ЛСК вида, мм). `angle` — направление выноски в ГРАДУСАХ (внутри → радианы). `viewNumber`: 0 = первый/главный. Значение (= радиус) измеряется автоматически. Возвращает радиус и номер вида. |
|
||||
| `drawing_add_angular_dimension` | `xc,yc` (вершина), `x1,y1` (точка на стороне 1), `x2,y2` (точка на стороне 2), `x3,y3` (положение размерной дуги — задаёт сектор), `angleType="min"`, `viewNumber=0` | Угловой размер между лучами (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) (ЛСК вида, мм). `x3,y3` должны лежать в измеряемом секторе. `angleType`: min/max/more. Значение (градусы) измеряется автоматически. Возвращает угол (°) и номер вида. |
|
||||
|
||||
Углы выноски (`angle`) экспонируются в **градусах** (LLM-дружелюбно), внутри `·π/180`.
|
||||
|
||||
## Архитектура
|
||||
|
||||
В существующем `DrawingService` (рядом с `AddDiametralDimensionAsync`); переиспользуем
|
||||
`RequireSymbols2DContainer(viewNumber)`, `DrawingDimensionResult`, паттерн отката.
|
||||
|
||||
- `DrawingService.AddRadialDimensionAsync(viewNumber, xc, yc, radius, angleDeg, ct)`.
|
||||
- `DrawingService.AddAngularDimensionAsync(viewNumber, xc, yc, x1, y1, x2, y2, x3, y3, angleType, ct)`.
|
||||
- Инструменты `drawing_add_radial_dimension`, `drawing_add_angular_dimension` в `DrawingTools`.
|
||||
- Helpers `GetViewRadialDimensionCountAsync`, `GetViewAngularDimensionCountAsync` (для тестов —
|
||||
попадание в вид; размеры в `ISymbols2DContainer.RadialDimensions/AngleDimensions.Count`, не в
|
||||
`IView.ObjectCount`).
|
||||
- Новый файл `src/Kompas.Mcp.Core/Drawings/AngleDimensionType.cs`: `enum AngleDimensionType {Min,Max,More}`
|
||||
+ static `AngleDimensionTypes.Parse(string)` / `ToKompas(...)` (→ `ksADMinAngle/ksADMaxAngle/ksADMoreAngle`),
|
||||
по образцу `DimensionOrientations`.
|
||||
|
||||
`DrawingDimensionResult` (Value + ViewNumber) переиспользуется: радиальный `Value`=радиус, угловой
|
||||
`Value`=градусы.
|
||||
|
||||
## Валидация (чистые static, unit-тест)
|
||||
|
||||
- Радиальный: `RequireFiniteCoords(xc, yc, angleDeg)` + `RequirePositiveRadius(radius)` — уже есть.
|
||||
- Угловой: `RequireFiniteCoords(xc,yc,x1,y1,x2,y2,x3,y3)` (8 координат) + `RequireDistinctPoints`
|
||||
четырежды: вершина≠(x1,y1), вершина≠(x2,y2), вершина≠(x3,y3) (иначе дуга нулевого радиуса),
|
||||
(x1,y1)≠(x2,y2) (#1: совпавшие точки сторон → одинаковые лучи). Все уже есть; новых валидаторов не требуется.
|
||||
|
||||
## Реализация
|
||||
|
||||
**`AddRadialDimensionCore`** (зеркало диаметрального, но значение = радиус): валидация → `RequireSymbols2DContainer`
|
||||
→ `RadialDimensions.Add()` (null-check) → `Xc/Yc/Radius`, `Angle=DimensionAngles.ToRadians(angleDeg)` (#6), `DimensionType=true`
|
||||
(стиль R от центра, явно), `AutoNominalValue=true` → `Update()` (FALSE → откат `Delete`+ошибка) →
|
||||
`Valid` (false → откат) → `value = NominalValue` (радиус); `value<=0 || !IsFinite` → откат+ошибка →
|
||||
`DrawingDimensionResult{Value, ViewNumber}`.
|
||||
|
||||
**`AddAngularDimensionCore`**: валидация → `RequireSymbols2DContainer` → `AngleDimensions.Add(ksDrADimension)`
|
||||
(null-check) → `Xc/Yc/X1/Y1/X2/Y2/X3/Y3`, `DimensionType=AngleDimensionTypes.ToKompas(angleType)`,
|
||||
`AutoNominalValue=true` → `Update()` (FALSE → откат) → `Valid` (false → откат) → `value=NominalValue`
|
||||
(°); `value<=0 || !IsFinite` → откат+ошибка с подсказкой (вырожденный угол: стороны коллинеарны или
|
||||
`x3,y3` вне сектора) → `DrawingDimensionResult{Value, ViewNumber}`. Угол в (0,360); защиту по верхней
|
||||
границе не форсируем (КОМПАС нормализует, `more` может дать >180).
|
||||
|
||||
RCW точечно не освобождаем (консистентно с линейным/диаметральным; долг v2-2).
|
||||
|
||||
## Тестирование
|
||||
|
||||
### Unit (`DrawingDimensionTypeTests` — новый, либо в `DrawingValidationTests`)
|
||||
- `AngleDimensionTypes.Parse`: `min/max/more` (+ рус. синонимы) → enum; неизвестное → `ArgumentException`.
|
||||
- `AngleDimensionTypes.ToKompas`: маппинг трёх значений → `ksADMinAngle/ksADMaxAngle/ksADMoreAngle`.
|
||||
- (`ToRadians`, `RequirePositiveRadius`, координаты — уже покрыты.)
|
||||
|
||||
### Integration (`DrawingRadialTests`, `DrawingAngularTests`; наследуют `IntegrationTestBase`)
|
||||
Радиальный:
|
||||
1. **Значение = радиус**: `radius=12 → Value ≈ 12` (промотированный спайк).
|
||||
2. **Попадание в вид**: `RadialDimensions.Count` целевого вида +1 (адресация по `viewNumber`).
|
||||
3. **Радиус ≤ 0** → `ArgumentOutOfRangeException` (до `Add`).
|
||||
4. **Нет видов** → понятная ошибка.
|
||||
|
||||
Угловой:
|
||||
5. **Измеряет 90°**: вершина(0,0), (10,0)&(0,10), x3/y3=(5,5) → `Value ≈ 90`.
|
||||
6. **Измеряет 45°**: вершина(0,0), (10,0)&(10,10), x3/y3=(8,4) → `Value ≈ 45`.
|
||||
7. **Попадание в вид**: `AngleDimensions.Count` целевого вида +1.
|
||||
8. **Выбор угла типом** (#2): та же геометрия 90°, но `angleType=More` → `Value ≈ 270`
|
||||
(документированное поведение: какой из двух углов измерять, выбирает `angleType`, не `x3,y3`).
|
||||
9. **Вырожденный угол до Add** (вершина совпадает с точкой стороны) → `ArgumentException`.
|
||||
10. **Откат после Add** (#3): коллинеарные сонаправленные лучи (0,0),(10,0),(20,0) → угол ≈0 →
|
||||
`InvalidOperationException`; `AngleDimensions.Count` целевого вида НЕ изменился (откат `Delete`).
|
||||
11. **Несуществующий `viewNumber>0`** (#4) → понятная ошибка (ветка `FindView`).
|
||||
12. **Нет видов** → понятная ошибка.
|
||||
|
||||
(Радиальный — аналогично добавлен тест на несуществующий `viewNumber>0`, #4.)
|
||||
|
||||
## Дальнейшее (вне спека)
|
||||
- Ассоциативная привязка размеров к геометрии вида (`BaseObject`/`BaseObject1/2`); рамка/формат листа;
|
||||
текстовые обозначения (шероховатость `Roughs`, допуски формы `Tolerances`, выноски `Leaders`,
|
||||
тех. требования); радиальный с изломом (`BreakRadialDimensions`), угловой с обрывом (`ksDrABreakDimension`).
|
||||
@@ -0,0 +1,42 @@
|
||||
using Kompas6Constants;
|
||||
|
||||
namespace Kompas.Mcp.Core.Drawings;
|
||||
|
||||
/// <summary>Тип углового размера — какой из двух секторов измерять
|
||||
/// (обёртка над <see cref="ksAngleDimTypeEnum"/>).</summary>
|
||||
public enum AngleDimensionType
|
||||
{
|
||||
/// <summary>Минимальный (острый) угол.</summary>
|
||||
Min,
|
||||
/// <summary>Максимальный (тупой) угол.</summary>
|
||||
Max,
|
||||
/// <summary>Угол более 180° (рефлексный).</summary>
|
||||
More,
|
||||
}
|
||||
|
||||
/// <summary>Маппинг типа углового размера ↔ перечисление КОМПАС и разбор строки.</summary>
|
||||
public static class AngleDimensionTypes
|
||||
{
|
||||
public static ksAngleDimTypeEnum ToKompas(AngleDimensionType type) => type switch
|
||||
{
|
||||
AngleDimensionType.Min => ksAngleDimTypeEnum.ksADMinAngle,
|
||||
AngleDimensionType.Max => ksAngleDimTypeEnum.ksADMaxAngle,
|
||||
AngleDimensionType.More => ksAngleDimTypeEnum.ksADMoreAngle,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "Неизвестный тип углового размера."),
|
||||
};
|
||||
|
||||
/// <summary>Разобрать строку типа углового размера (англ./рус.) в <see cref="AngleDimensionType"/>.</summary>
|
||||
public static AngleDimensionType Parse(string value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
return value.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"min" or "minimal" or "минимальный" or "острый" => AngleDimensionType.Min,
|
||||
"max" or "maximal" or "максимальный" or "тупой" => AngleDimensionType.Max,
|
||||
"more" or "more180" or "больше180" or "рефлексный" => AngleDimensionType.More,
|
||||
_ => throw new ArgumentException(
|
||||
$"Неизвестный тип углового размера '{value}'. Допустимо: min, max, more.",
|
||||
nameof(value)),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ namespace Kompas.Mcp.Core.Drawings;
|
||||
/// <summary>Результат простановки размера на чертеже.</summary>
|
||||
public sealed record DrawingDimensionResult
|
||||
{
|
||||
/// <summary>Измеренное значение размера (мм).</summary>
|
||||
/// <summary>Измеренное значение размера: линейный/диаметральный/радиальный — мм, угловой — градусы.</summary>
|
||||
public required double Value { get; init; }
|
||||
|
||||
/// <summary>Номер вида, на котором поставлен размер.</summary>
|
||||
|
||||
@@ -184,6 +184,109 @@ public sealed class DrawingService
|
||||
public Task<int> GetViewDiametralDimensionCountAsync(int viewNumber, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.DiametralDimensions?.Count ?? 0, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Поставить радиальный размер (R) окружности/дуги с центром (xc,yc) и радиусом <paramref name="radius"/>
|
||||
/// (ЛОКАЛЬНАЯ СК вида, мм) на виде. <paramref name="angleDeg"/> — направление выноски (градусы).
|
||||
/// Значение (= радиус) измеряется автоматически. Возвращает радиус и номер вида.
|
||||
/// </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);
|
||||
|
||||
private DrawingDimensionResult AddRadialDimensionCore(int viewNumber, double xc, double yc,
|
||||
double radius, double angleDeg)
|
||||
{
|
||||
DrawingValidation.RequireFiniteCoords(xc, yc);
|
||||
DrawingValidation.RequirePositiveRadius(radius);
|
||||
var angleRad = DimensionAngles.ToRadians(angleDeg);
|
||||
|
||||
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
|
||||
var dim = symbols.RadialDimensions?.Add()
|
||||
?? throw new InvalidOperationException("RadialDimensions.Add вернул null.");
|
||||
try
|
||||
{
|
||||
dim.Xc = xc; dim.Yc = yc; dim.Radius = radius; dim.Angle = angleRad;
|
||||
dim.DimensionType = true; // стиль R «от центра» (на величину не влияет — спайк)
|
||||
((IDimensionText)dim).AutoNominalValue = true;
|
||||
|
||||
if (!dim.Update())
|
||||
throw new InvalidOperationException("IRadialDimension.Update вернул FALSE — размер не построен.");
|
||||
if (!dim.Valid)
|
||||
throw new InvalidOperationException("Радиальный размер построен невалидным.");
|
||||
|
||||
var value = ((IDimensionText)dim).NominalValue; // = радиус (не диаметр — спайк)
|
||||
if (value <= 0 || !double.IsFinite(value)) // инвариант радиуса: строго положительный
|
||||
throw new InvalidOperationException($"Радиальный размер измерил некорректное значение ({value}).");
|
||||
return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
|
||||
}
|
||||
catch
|
||||
{
|
||||
try { dim.Delete(); } catch { /* лучшее усилие */ }
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Число радиальных размеров на виде (для проверки попадания в вид — они в
|
||||
/// ISymbols2DContainer, а не в IView.ObjectCount).</summary>
|
||||
public Task<int> GetViewRadialDimensionCountAsync(int viewNumber, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.RadialDimensions?.Count ?? 0, ct);
|
||||
|
||||
/// <summary>
|
||||
/// Поставить угловой размер между лучами (xc,yc)→(x1,y1) и (xc,yc)→(x2,y2) (ЛОКАЛЬНАЯ СК вида, мм)
|
||||
/// на виде. (x3,y3) — положение размерной дуги (задаёт её радиус = расстояние от вершины).
|
||||
/// <paramref name="angleType"/> выбирает, какой из двух углов измерять (min/max/more) — например
|
||||
/// 90° или 270° для перпендикуляра. Значение (градусы) измеряется автоматически. Возвращает угол (°) и номер вида.
|
||||
/// </summary>
|
||||
public Task<DrawingDimensionResult> AddAngularDimensionAsync(int viewNumber, double xc, double yc,
|
||||
double x1, double y1, double x2, double y2, double x3, double y3,
|
||||
AngleDimensionType angleType, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
AddAngularDimensionCore(viewNumber, xc, yc, x1, y1, x2, y2, x3, y3, angleType), ct);
|
||||
|
||||
private DrawingDimensionResult AddAngularDimensionCore(int viewNumber, double xc, double yc,
|
||||
double x1, double y1, double x2, double y2, double x3, double y3, AngleDimensionType angleType)
|
||||
{
|
||||
DrawingValidation.RequireFiniteCoords(xc, yc, x1, y1, x2, y2, x3, y3);
|
||||
DrawingValidation.RequireDistinctPoints(xc, yc, x1, y1); // вершина ≠ точка стороны 1
|
||||
DrawingValidation.RequireDistinctPoints(xc, yc, x2, y2); // вершина ≠ точка стороны 2
|
||||
DrawingValidation.RequireDistinctPoints(xc, yc, x3, y3); // вершина ≠ положение дуги (R дуги > 0)
|
||||
DrawingValidation.RequireDistinctPoints(x1, y1, x2, y2); // стороны не совпадают (одинаковые лучи)
|
||||
|
||||
var (view, symbols) = RequireSymbols2DContainer(viewNumber);
|
||||
var dim = symbols.AngleDimensions?.Add(DrawingObjectTypeEnum.ksDrADimension)
|
||||
?? throw new InvalidOperationException("AngleDimensions.Add вернул null.");
|
||||
try
|
||||
{
|
||||
dim.Xc = xc; dim.Yc = yc;
|
||||
dim.X1 = x1; dim.Y1 = y1; dim.X2 = x2; dim.Y2 = y2; dim.X3 = x3; dim.Y3 = y3;
|
||||
dim.DimensionType = AngleDimensionTypes.ToKompas(angleType);
|
||||
((IDimensionText)dim).AutoNominalValue = true;
|
||||
|
||||
if (!dim.Update())
|
||||
throw new InvalidOperationException("IAngleDimension.Update вернул FALSE — размер не построен.");
|
||||
if (!dim.Valid)
|
||||
throw new InvalidOperationException("Угловой размер построен невалидным.");
|
||||
|
||||
var value = ((IDimensionText)dim).NominalValue; // градусы
|
||||
// Нулевой/некорректный угол — стороны коллинеарны (сонаправлены) или (x3,y3) вне сектора.
|
||||
if (value <= 0 || !double.IsFinite(value))
|
||||
throw new InvalidOperationException(
|
||||
$"Угловой размер измерил некорректное значение ({value}°). Стороны коллинеарны или " +
|
||||
"положение дуги (x3,y3) задаёт вырожденный сектор.");
|
||||
return new DrawingDimensionResult { Value = value, ViewNumber = view.Number };
|
||||
}
|
||||
catch
|
||||
{
|
||||
try { dim.Delete(); } catch { /* лучшее усилие */ }
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Число угловых размеров на виде (для проверки попадания в вид — они в
|
||||
/// ISymbols2DContainer, а не в IView.ObjectCount).</summary>
|
||||
public Task<int> GetViewAngularDimensionCountAsync(int viewNumber, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() => RequireSymbols2DContainer(viewNumber).Symbols.AngleDimensions?.Count ?? 0, ct);
|
||||
|
||||
/// <summary>Вид + его ISymbols2DContainer (QI от вида). Общий для линейных/диаметральных/… размеров.</summary>
|
||||
private (IView View, ISymbols2DContainer Symbols) RequireSymbols2DContainer(int viewNumber)
|
||||
{
|
||||
|
||||
@@ -83,4 +83,48 @@ public sealed class DrawingTools(KompasSession session, DrawingService drawing)
|
||||
var r = await drawing.AddDiametralDimensionAsync(viewNumber, xc, yc, radius, angle);
|
||||
return $"Диаметральный размер поставлен на виде {r.ViewNumber}, Ø{r.Value:0.###} мм.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "drawing_add_radial_dimension")]
|
||||
[Description("Поставить радиальный размер (R) окружности/дуги на виде активного чертежа. Окружность " +
|
||||
"задаётся центром (xc,yc) и радиусом radius в ЛОКАЛЬНОЙ СК вида (мм; при масштабе 1:1 = координаты " +
|
||||
"модели на плоскости вида) — задайте параметры реальной окружности вида. angle — направление выноски " +
|
||||
"в градусах. viewNumber — номер вида из drawing_create_standard_views (0 = первый/главный). " +
|
||||
"Значение (= радиус) измеряется автоматически. Возвращает радиус и номер вида.")]
|
||||
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)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
var r = await drawing.AddRadialDimensionAsync(viewNumber, xc, yc, radius, angle);
|
||||
return $"Радиальный размер поставлен на виде {r.ViewNumber}, R{r.Value:0.###} мм.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "drawing_add_angular_dimension")]
|
||||
[Description("Поставить угловой размер между двумя сторонами на виде активного чертежа. Угол задаётся " +
|
||||
"лучами из вершины (xc,yc): сторона 1 проходит через точку (x1,y1), сторона 2 — через (x2,y2) " +
|
||||
"(всё в ЛОКАЛЬНОЙ СК вида, мм). Точка (x3,y3) — положение размерной дуги (задаёт её радиус = " +
|
||||
"расстояние от вершины); поместите её в стороне измеряемого угла. КАКОЙ из двух углов измерять " +
|
||||
"(например 90° или 270° для перпендикуляра) — выбирает angleType: min (меньший/острый) | max " +
|
||||
"(больший/тупой) | more (рефлексный, >180°). viewNumber — номер вида (0 = первый/главный). " +
|
||||
"Значение (градусы) измеряется автоматически. Возвращает угол (°) и номер вида.")]
|
||||
public async Task<string> AddAngularDimension(
|
||||
[Description("X вершины угла (СК вида, мм)")] double xc,
|
||||
[Description("Y вершины угла")] double yc,
|
||||
[Description("X точки на стороне 1")] double x1,
|
||||
[Description("Y точки на стороне 1")] double y1,
|
||||
[Description("X точки на стороне 2")] double x2,
|
||||
[Description("Y точки на стороне 2")] double y2,
|
||||
[Description("X положения размерной дуги (внутри измеряемого сектора)")] double x3,
|
||||
[Description("Y положения размерной дуги")] double y3,
|
||||
[Description("Тип угла: min | max | more")] string angleType = "min",
|
||||
[Description("Номер вида (0 = первый/главный)")] int viewNumber = 0)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
var t = AngleDimensionTypes.Parse(angleType);
|
||||
var r = await drawing.AddAngularDimensionAsync(viewNumber, xc, yc, x1, y1, x2, y2, x3, y3, t);
|
||||
return $"Угловой размер поставлен на виде {r.ViewNumber}, {r.Value:0.###}°.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Kompas.Mcp.Core.Drawings;
|
||||
|
||||
namespace Kompas.Mcp.Tests;
|
||||
|
||||
[Trait("Category", "Unit")]
|
||||
public sealed class AngleDimensionTypesTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("min", AngleDimensionType.Min)]
|
||||
[InlineData("max", AngleDimensionType.Max)]
|
||||
[InlineData("more", AngleDimensionType.More)]
|
||||
[InlineData(" MIN ", AngleDimensionType.Min)]
|
||||
[InlineData("минимальный", AngleDimensionType.Min)]
|
||||
[InlineData("максимальный", AngleDimensionType.Max)]
|
||||
[InlineData("больше180", AngleDimensionType.More)]
|
||||
public void Parse_maps_known(string value, AngleDimensionType expected)
|
||||
=> Assert.Equal(expected, AngleDimensionTypes.Parse(value));
|
||||
|
||||
[Fact]
|
||||
public void Parse_throws_on_unknown()
|
||||
=> Assert.Throws<ArgumentException>(() => AngleDimensionTypes.Parse("acute"));
|
||||
|
||||
[Fact]
|
||||
public void Parse_throws_on_null()
|
||||
=> Assert.Throws<ArgumentNullException>(() => AngleDimensionTypes.Parse(null!));
|
||||
|
||||
// ToKompas (→ ksAngleDimTypeEnum) намеренно НЕ покрыт unit-тестом: тестовый проект не ссылается
|
||||
// на Kompas6Constants (граница проекта, как у DimensionOrientations.ToKompas). Маппинг тривиален
|
||||
// и проверяется интеграцией (min → острый угол 90°, выбор сектора).
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Drawings;
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
|
||||
namespace Kompas.Mcp.Tests.Integration;
|
||||
|
||||
/// <summary>Интеграция: угловой размер на виде (API7 ISymbols2DContainer.AngleDimensions).</summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Collection(KompasCollection.Name)]
|
||||
public sealed class DrawingAngularTests : IntegrationTestBase
|
||||
{
|
||||
private readonly DocumentService _docs;
|
||||
private readonly PartModeler _modeler;
|
||||
private readonly DrawingService _drawing;
|
||||
|
||||
public DrawingAngularTests(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> BuildAndSaveBoxAsync()
|
||||
{
|
||||
var path = TestPaths.NewFile(".m3d");
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddRectangleAsync(s, 0, 0, 40, 30);
|
||||
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;
|
||||
}
|
||||
|
||||
private async Task<string> NewDrawingWithViewsAsync()
|
||||
{
|
||||
var partFile = await BuildAndSaveBoxAsync();
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
|
||||
return partFile;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Angular_measures_right_angle()
|
||||
{
|
||||
await NewDrawingWithViewsAsync();
|
||||
try
|
||||
{
|
||||
// Вершина (0,0); сторона1 → (10,0); сторона2 → (0,10); дуга в секторе (5,5) → 90°.
|
||||
var r = await _drawing.AddAngularDimensionAsync(
|
||||
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 0, y2: 10, x3: 5, y3: 5,
|
||||
angleType: AngleDimensionType.Min);
|
||||
Assert.InRange(r.Value, 89.5, 90.5);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Angular_measures_45_degrees()
|
||||
{
|
||||
await NewDrawingWithViewsAsync();
|
||||
try
|
||||
{
|
||||
// Вершина (0,0); сторона1 → (10,0); сторона2 → (10,10) (45°); дуга (8,4).
|
||||
var r = await _drawing.AddAngularDimensionAsync(
|
||||
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 10, y2: 10, x3: 8, y3: 4,
|
||||
angleType: AngleDimensionType.Min);
|
||||
Assert.InRange(r.Value, 44.5, 45.5);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Targets_specific_view_and_increments_angular_count()
|
||||
{
|
||||
var partFile = await BuildAndSaveBoxAsync();
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
try
|
||||
{
|
||||
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
|
||||
var targetNumber = views.ViewNumbers[1];
|
||||
|
||||
var before = await _drawing.GetViewAngularDimensionCountAsync(targetNumber);
|
||||
var r = await _drawing.AddAngularDimensionAsync(
|
||||
targetNumber, xc: 0, yc: 0, x1: 10, y1: 0, x2: 0, y2: 10, x3: 5, y3: 5,
|
||||
angleType: AngleDimensionType.Min);
|
||||
var after = await _drawing.GetViewAngularDimensionCountAsync(targetNumber);
|
||||
|
||||
Assert.Equal(targetNumber, r.ViewNumber);
|
||||
Assert.Equal(before + 1, after);
|
||||
Assert.InRange(r.Value, 89.5, 90.5);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Angle_type_more_measures_reflex_angle()
|
||||
{
|
||||
await NewDrawingWithViewsAsync();
|
||||
try
|
||||
{
|
||||
// Та же геометрия 90°, но angleType=more → измеряется рефлексный угол ~270°.
|
||||
// Тип угла (min/max/more) выбирает, какой из секторов измерять; (x3,y3) лишь
|
||||
// позиционирует размерную дугу (её ставим в рефлексный сектор для согласованности).
|
||||
var r = await _drawing.AddAngularDimensionAsync(
|
||||
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 0, y2: 10, x3: -5, y3: -5,
|
||||
angleType: AngleDimensionType.More);
|
||||
Assert.InRange(r.Value, 269.0, 271.0);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Coincident_vertex_and_side_throws()
|
||||
{
|
||||
await NewDrawingWithViewsAsync();
|
||||
try
|
||||
{
|
||||
// Вершина совпадает с точкой стороны1 → вырождение до Add.
|
||||
await Assert.ThrowsAsync<ArgumentException>(() =>
|
||||
_drawing.AddAngularDimensionAsync(
|
||||
viewNumber: 0, xc: 0, yc: 0, x1: 0, y1: 0, x2: 0, y2: 10, x3: 5, y3: 5,
|
||||
angleType: AngleDimensionType.Min));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Collinear_rays_rollback_leaves_count_unchanged()
|
||||
{
|
||||
await NewDrawingWithViewsAsync();
|
||||
try
|
||||
{
|
||||
var before = await _drawing.GetViewAngularDimensionCountAsync(0);
|
||||
// Сонаправленные лучи (0,0)→(10,0) и (0,0)→(20,0): угол ≈0 → пост-проверка → откат.
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_drawing.AddAngularDimensionAsync(
|
||||
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 20, y2: 0, x3: 15, y3: 5,
|
||||
angleType: AngleDimensionType.Min));
|
||||
var after = await _drawing.GetViewAngularDimensionCountAsync(0);
|
||||
Assert.Equal(before, after);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task No_views_throws()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
try
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_drawing.AddAngularDimensionAsync(
|
||||
viewNumber: 0, xc: 0, yc: 0, x1: 10, y1: 0, x2: 0, y2: 10, x3: 5, y3: 5,
|
||||
angleType: AngleDimensionType.Min));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unknown_view_number_throws()
|
||||
{
|
||||
await NewDrawingWithViewsAsync();
|
||||
try
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_drawing.AddAngularDimensionAsync(
|
||||
viewNumber: 999, xc: 0, yc: 0, x1: 10, y1: 0, x2: 0, y2: 10, x3: 5, y3: 5,
|
||||
angleType: AngleDimensionType.Min));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using Kompas.Mcp.Core.Documents;
|
||||
using Kompas.Mcp.Core.Drawings;
|
||||
using Kompas.Mcp.Core.Modeling;
|
||||
|
||||
namespace Kompas.Mcp.Tests.Integration;
|
||||
|
||||
/// <summary>Интеграция: радиальный размер (R) на виде (API7 ISymbols2DContainer.RadialDimensions).</summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Collection(KompasCollection.Name)]
|
||||
public sealed class DrawingRadialTests : IntegrationTestBase
|
||||
{
|
||||
private readonly DocumentService _docs;
|
||||
private readonly PartModeler _modeler;
|
||||
private readonly DrawingService _drawing;
|
||||
|
||||
public DrawingRadialTests(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> BuildAndSaveBoxAsync()
|
||||
{
|
||||
var path = TestPaths.NewFile(".m3d");
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddRectangleAsync(s, 0, 0, 40, 30);
|
||||
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 Radial_dimension_value_is_radius()
|
||||
{
|
||||
var partFile = await BuildAndSaveBoxAsync();
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
try
|
||||
{
|
||||
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
|
||||
// Радиус 12 → значение радиального = радиус (НЕ диаметр), подтверждено спайком.
|
||||
var r = await _drawing.AddRadialDimensionAsync(viewNumber: 0, xc: 10, yc: 10, radius: 12, angleDeg: 45);
|
||||
Assert.InRange(r.Value, 11.9, 12.1);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Targets_specific_view_and_increments_radial_count()
|
||||
{
|
||||
var partFile = await BuildAndSaveBoxAsync();
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
try
|
||||
{
|
||||
var views = await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
|
||||
var targetNumber = views.ViewNumbers[1];
|
||||
|
||||
var before = await _drawing.GetViewRadialDimensionCountAsync(targetNumber);
|
||||
var r = await _drawing.AddRadialDimensionAsync(targetNumber, xc: 5, yc: 5, radius: 8, angleDeg: 30);
|
||||
var after = await _drawing.GetViewRadialDimensionCountAsync(targetNumber);
|
||||
|
||||
Assert.Equal(targetNumber, r.ViewNumber);
|
||||
Assert.Equal(before + 1, after);
|
||||
Assert.InRange(r.Value, 7.9, 8.1);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Non_positive_radius_throws()
|
||||
{
|
||||
var partFile = await BuildAndSaveBoxAsync();
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
try
|
||||
{
|
||||
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
|
||||
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(() =>
|
||||
_drawing.AddRadialDimensionAsync(viewNumber: 0, xc: 0, yc: 0, radius: 0, angleDeg: 0));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task No_views_throws()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
try
|
||||
{
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_drawing.AddRadialDimensionAsync(viewNumber: 0, xc: 0, yc: 0, radius: 10, angleDeg: 0));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Unknown_view_number_throws()
|
||||
{
|
||||
var partFile = await BuildAndSaveBoxAsync();
|
||||
await _docs.CreateAsync(KompasDocumentType.Drawing);
|
||||
try
|
||||
{
|
||||
await _drawing.CreateStandardViewsAsync(partFile, 1.0, 100, 150);
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_drawing.AddRadialDimensionAsync(viewNumber: 999, xc: 0, yc: 0, radius: 10, angleDeg: 0));
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user