diff --git a/docs/superpowers/specs/2026-05-26-sweep-design.md b/docs/superpowers/specs/2026-05-26-sweep-design.md
new file mode 100644
index 0000000..f4fc735
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-26-sweep-design.md
@@ -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) по факту, сохранив проверку объёма.
diff --git a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs
index ce3b205..c9bd871 100644
--- a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs
+++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs
@@ -266,6 +266,39 @@ public sealed partial class PartModeler
return id;
}, ct);
+ /// Кинематическая операция (sweep): переместить профиль (эскиз profileSketchId)
+ /// вдоль траектории (эскиз pathSketchId), образуя сплошное тело. Возвращает id операции.
+ public Task 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);
+
/// Перестроить документ.
public Task RebuildAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
diff --git a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs
index 5ded76b..cd050e3 100644
--- a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs
+++ b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs
@@ -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 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 Rebuild()
diff --git a/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs
index 5429d0c..c5c457c 100644
--- a/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs
+++ b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs
@@ -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); }
+ }
}