feat(feature): кинематическая операция (sweep)
SweepAsync (o3d_baseEvolution): SetSketch(профиль) + PathPartArray(траектория) + sketchShiftType=0 + SetThinParam(false). Инструмент sweep в FeatureTools. Integration: окружность R5 по вертикали 50 → цилиндр ~3927 мм³. loft отложен — требует offset-плоскости (пакет E) для параллельных сечений. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# Дизайн: пакет B (шаг 3) — кинематическая операция (sweep)
|
||||
|
||||
**Дата:** 2026-05-26
|
||||
**Статус:** на ревью (автономный режим; ревьюер — Codex)
|
||||
|
||||
## Цель
|
||||
|
||||
Добавить формообразующую **кинематическую операцию** (sweep) — переместить профиль вдоль
|
||||
траектории, образуя тело. Третий шаг пакета B. Один MCP-инструмент `sweep`. В отличие от
|
||||
loft (требует параллельные сечения → offset-плоскость, пакет E), sweep естественно работает с
|
||||
базовыми плоскостями: профиль на одной, траектория на перпендикулярной.
|
||||
|
||||
## Сигнатуры COM (рефлексия interop)
|
||||
|
||||
```
|
||||
Obj3dType.o3d_baseEvolution = 45
|
||||
ksBaseEvolutionDefinition:
|
||||
short sketchShiftType // тип переноса сечения вдоль пути
|
||||
bool SetSketch(object) // профиль (сечение)
|
||||
object PathPartArray() // ksEntityCollection сегментов траектории
|
||||
bool SetThinParam(bool thin, short thinType, double normal, double reverse)
|
||||
```
|
||||
|
||||
**Паттерн:** `NewEntity(o3d_baseEvolution)` → `GetDefinition()` → `SetSketch(профиль)` →
|
||||
`PathPartArray().Add(траектория)` → `sketchShiftType` → `SetThinParam(false,…)` (сплошное) →
|
||||
`Create()`.
|
||||
|
||||
## Новый MCP-инструмент (группа Feature)
|
||||
|
||||
| Инструмент | Параметры | Поведение |
|
||||
|---|---|---|
|
||||
| `sweep` | `profileSketchId: int`, `pathSketchId: int` | Кинематическая операция: переместить замкнутый профиль `profileSketchId` вдоль траектории `pathSketchId`, образуя сплошное тело. Профиль и траектория — на разных (обычно перпендикулярных) плоскостях; начало траектории совпадает с плоскостью профиля. Возвращает id операции. |
|
||||
|
||||
**Решения (YAGNI):** тонкостенный режим (`thin`) и `sketchShiftType` наружу не выставляем —
|
||||
сплошное тело, `sketchShiftType=0` (перенос с сохранением угла). Можно расширить позже.
|
||||
|
||||
## Реализация
|
||||
|
||||
- **Метод** `SweepAsync(int profileSketchId, int pathSketchId, CancellationToken)` в
|
||||
`PartModeler.Features.cs`. Оба эскиза закрываются перед операцией (`CloseSketchCore`);
|
||||
берутся `sketch.Entity` из реестра. `profileSketchId == pathSketchId` → `ArgumentException`.
|
||||
Возвращает id операции.
|
||||
- **Инструмент** `sweep` в `FeatureTools.cs`.
|
||||
- `PathPartArray()` проверяется на null (`as ksEntityCollection ?? throw`).
|
||||
- Транзитный `ksBaseEvolutionDefinition` не освобождается — консистентно (долг v2-2).
|
||||
- `Create()==FALSE` → `InvalidOperationException` (профиль/траектория несовместимы: начало пути
|
||||
не на плоскости профиля, самопересечение?).
|
||||
|
||||
## Тестирование
|
||||
|
||||
**Integration** (`FeatureOpsTests`): профиль — окружность R5 на XOY (центр 0,0); траектория —
|
||||
вертикальный отрезок на XOZ `(0,0)→(0,50)` (начинается в центре профиля, идёт по +Z) →
|
||||
`SweepAsync(profile, path)` → цилиндр R5×H50, объём `π·25·50 ≈ 3927 мм³`. Проверка
|
||||
`InRange(volume, 3927·0.95, 3927·1.05)`. Если `sketchShiftType=0` не строит — перебрать тип
|
||||
(0/1) по факту, сохранив проверку объёма.
|
||||
@@ -266,6 +266,39 @@ public sealed partial class PartModeler
|
||||
return id;
|
||||
}, ct);
|
||||
|
||||
/// <summary>Кинематическая операция (sweep): переместить профиль (эскиз profileSketchId)
|
||||
/// вдоль траектории (эскиз pathSketchId), образуя сплошное тело. Возвращает id операции.</summary>
|
||||
public Task<int> SweepAsync(int profileSketchId, int pathSketchId, CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
{
|
||||
if (profileSketchId == pathSketchId)
|
||||
throw new ArgumentException("Профиль и траектория должны быть разными эскизами.", nameof(pathSketchId));
|
||||
var profile = RequireSketch(profileSketchId);
|
||||
var path = RequireSketch(pathSketchId);
|
||||
CloseSketchCore(profile); // оба эскиза должны быть закрыты перед операцией
|
||||
CloseSketchCore(path);
|
||||
|
||||
var part = GetTopPart();
|
||||
var entity = part.NewEntity((short)Obj3dType.o3d_baseEvolution) as ksEntity
|
||||
?? throw new InvalidOperationException("NewEntity(o3d_baseEvolution) вернул null.");
|
||||
var def = entity.GetDefinition() as ksBaseEvolutionDefinition
|
||||
?? throw new InvalidOperationException("GetDefinition() кинематической операции вернул не ksBaseEvolutionDefinition.");
|
||||
def.SetSketch(profile.Entity);
|
||||
var pathArr = def.PathPartArray() as ksEntityCollection
|
||||
?? throw new InvalidOperationException("PathPartArray() вернул не ksEntityCollection.");
|
||||
pathArr.Add(path.Entity);
|
||||
def.sketchShiftType = 0; // перенос сечения с сохранением угла
|
||||
def.SetThinParam(false, 0, 0, 0); // сплошное тело
|
||||
|
||||
if (!entity.Create())
|
||||
throw new InvalidOperationException(
|
||||
"Create() кинематической операции вернул FALSE (начало траектории не на плоскости профиля или несовместимая геометрия?).");
|
||||
|
||||
var id = _nextId++;
|
||||
_features[id] = entity;
|
||||
return id;
|
||||
}, ct);
|
||||
|
||||
/// <summary>Перестроить документ.</summary>
|
||||
public Task RebuildAsync(CancellationToken ct = default)
|
||||
=> _dispatcher.InvokeAsync(() =>
|
||||
|
||||
@@ -113,6 +113,15 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler)
|
||||
return $"Ребро жёсткости толщиной {thickness} мм создано (side={side}), id={id}.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "sweep")]
|
||||
[Description("Кинематическая операция: переместить замкнутый профиль (эскиз profileSketchId) вдоль траектории (эскиз pathSketchId), образуя сплошное тело. Профиль и траектория — на разных плоскостях (обычно перпендикулярных); начало траектории должно совпадать с плоскостью профиля. Возвращает id операции.")]
|
||||
public async Task<string> Sweep(int profileSketchId, int pathSketchId)
|
||||
{
|
||||
await session.ConnectAsync();
|
||||
var id = await modeler.SweepAsync(profileSketchId, pathSketchId);
|
||||
return $"Кинематическая операция создана (профиль {profileSketchId} по траектории {pathSketchId}), id={id}.";
|
||||
}
|
||||
|
||||
[McpServerTool(Name = "rebuild")]
|
||||
[Description("Перестроить активный документ.")]
|
||||
public async Task<string> Rebuild()
|
||||
|
||||
@@ -81,4 +81,31 @@ public sealed class FeatureOpsTests
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Sweep_circle_along_line_makes_cylinder()
|
||||
{
|
||||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||||
try
|
||||
{
|
||||
// Профиль: окружность R5 на XOY (центр 0,0).
|
||||
var profile = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||||
await _modeler.AddCircleAsync(profile, 0, 0, 5);
|
||||
await _modeler.CloseSketchAsync(profile);
|
||||
// Траектория: вертикальный отрезок (0,0)→(0,50) на XOZ (по +Z из центра профиля).
|
||||
var path = await _modeler.OpenSketchAsync(BasePlane.XOZ);
|
||||
await _modeler.AddLineAsync(path, 0, 0, 0, 50);
|
||||
await _modeler.CloseSketchAsync(path);
|
||||
|
||||
var id = await _modeler.SweepAsync(profile, path);
|
||||
Assert.True(id > 0);
|
||||
await _modeler.RebuildAsync();
|
||||
|
||||
// Окружность R5 по прямой длиной 50 → цилиндр: π·25·50 ≈ 3927 мм³.
|
||||
var v = (await _query.GetPartInfoAsync()).Volume;
|
||||
var expected = Math.PI * 25 * 50;
|
||||
Assert.InRange(v, expected * 0.95, expected * 1.05);
|
||||
}
|
||||
finally { await _docs.CloseAsync(save: false); }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user