diff --git a/src/Kompas.Mcp.Core/Modeling/CoordinateAxis.cs b/src/Kompas.Mcp.Core/Modeling/CoordinateAxis.cs
new file mode 100644
index 0000000..18a9042
--- /dev/null
+++ b/src/Kompas.Mcp.Core/Modeling/CoordinateAxis.cs
@@ -0,0 +1,30 @@
+using Kompas6Constants3D;
+
+namespace Kompas.Mcp.Core.Modeling;
+
+/// Базовая координатная ось (для направления массива / оси вращения).
+public enum CoordinateAxis
+{
+ X,
+ Y,
+ Z,
+}
+
+public static class CoordinateAxes
+{
+ public static Obj3dType ToObj3dType(CoordinateAxis axis) => axis switch
+ {
+ CoordinateAxis.X => Obj3dType.o3d_axisOX,
+ CoordinateAxis.Y => Obj3dType.o3d_axisOY,
+ CoordinateAxis.Z => Obj3dType.o3d_axisOZ,
+ _ => throw new ArgumentOutOfRangeException(nameof(axis), axis, "Неизвестная ось."),
+ };
+
+ public static CoordinateAxis Parse(string value) => value?.Trim().ToUpperInvariant() switch
+ {
+ "X" => CoordinateAxis.X,
+ "Y" => CoordinateAxis.Y,
+ "Z" => CoordinateAxis.Z,
+ _ => throw new ArgumentException($"Неизвестная ось '{value}'. Допустимо: X, Y, Z.", nameof(value)),
+ };
+}
diff --git a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs
index b8dbd0e..0f57fd2 100644
--- a/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs
+++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.Features.cs
@@ -331,6 +331,46 @@ public sealed partial class PartModeler
return id;
}, ct);
+ /// Линейный массив (по сетке вдоль одной координатной оси): размножить операции
+ /// featureIds вдоль оси axis, count экземпляров всего (включая исходный, ≥2), шаг step (мм)
+ /// между соседними. geometric=true — быстрое геометрическое копирование. Возвращает id операции.
+ public Task LinearPatternAsync(IReadOnlyList featureIds, CoordinateAxis axis, int count, double step, bool geometric = false, CancellationToken ct = default)
+ => _dispatcher.InvokeAsync(() =>
+ {
+ ArgumentNullException.ThrowIfNull(featureIds);
+ var ids = featureIds.Distinct().ToList();
+ if (ids.Count == 0) throw new ArgumentException("Нужна хотя бы одна операция для массива.", nameof(featureIds));
+ SketchGeometry.RequireMin(count, 2, nameof(count)); // count включает исходный
+ SketchGeometry.RequirePositive(step, nameof(step)); // ловит и NaN
+
+ // Собираем источники до мутации модели (проверка существования всех id).
+ var features = ids.Select(RequireFeature).ToList();
+
+ var part = GetTopPart();
+ var axisEntity = part.GetDefaultEntity((short)CoordinateAxes.ToObj3dType(axis)) as ksEntity
+ ?? throw new InvalidOperationException("Не удалось получить координатную ось.");
+
+ var entity = part.NewEntity((short)Obj3dType.o3d_meshCopy) as ksEntity
+ ?? throw new InvalidOperationException("NewEntity(o3d_meshCopy) вернул null.");
+ var def = entity.GetDefinition() as ksMeshCopyDefinition
+ ?? throw new InvalidOperationException("GetDefinition() массива вернул не ksMeshCopyDefinition.");
+ def.SetAxis1(axisEntity);
+ def.SetCopyParamAlongAxis(true, 0, count, step, false); // ось 1: angle=0, factor=false (шаг между соседними)
+ def.count2 = 1; // второе направление выключено (1D-массив)
+ def.geomArray = geometric;
+ var arr = def.OperationArray() as ksEntityCollection
+ ?? throw new InvalidOperationException("OperationArray() массива вернул не ksEntityCollection.");
+ foreach (var f in features) arr.Add(f);
+
+ 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.Core/Modeling/PartModeler.cs b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs
index 353fb93..67de4ef 100644
--- a/src/Kompas.Mcp.Core/Modeling/PartModeler.cs
+++ b/src/Kompas.Mcp.Core/Modeling/PartModeler.cs
@@ -108,6 +108,11 @@ public sealed partial class PartModeler : IDisposable
=> _sketches.TryGetValue(id, out var s) ? s
: throw new KeyNotFoundException($"Эскиз с id={id} не найден.");
+ /// Получить ksEntity ранее созданной операции по её id (источник для массива/зеркала).
+ private ksEntity RequireFeature(int id)
+ => _features.TryGetValue(id, out var f) ? f
+ : throw new KeyNotFoundException($"Операция с id={id} не найдена.");
+
private SketchEntry RequireOpenSketch(int id)
{
var s = RequireSketch(id);
diff --git a/src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs b/src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs
index e849f38..1907fc6 100644
--- a/src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs
+++ b/src/Kompas.Mcp.Core/Modeling/SketchGeometry.cs
@@ -34,6 +34,13 @@ public static class SketchGeometry
throw new ArgumentOutOfRangeException(nameof(count), count, "Число вершин должно быть >= 3.");
}
+ /// Требовать целое значение не меньше (например, число экземпляров массива).
+ public static void RequireMin(int value, int min, string paramName)
+ {
+ if (value < min)
+ throw new ArgumentOutOfRangeException(paramName, value, $"Значение должно быть >= {min}.");
+ }
+
/// Валидировать список точек: не null, минимум , все координаты конечны.
public static void RequirePoints(IReadOnlyList<(double x, double y)> points, int min, string paramName)
{
diff --git a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs
index 8772cbf..b86cd3b 100644
--- a/src/Kompas.Mcp.Host/Tools/FeatureTools.cs
+++ b/src/Kompas.Mcp.Host/Tools/FeatureTools.cs
@@ -134,6 +134,20 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler)
return $"Операция по сечениям создана ({sections} сечений), id={id}.";
}
+ [McpServerTool(Name = "linear_pattern")]
+ [Description("Линейный массив: размножить ранее созданные операции featureIds (их id из extrude/revolve/fillet/...) вдоль координатной оси axis (X|Y|Z). count — количество экземпляров ВСЕГО, включая исходный (>=2). step — шаг между соседними экземплярами (мм). geometric=true — быстрое геометрическое копирование (может не строиться на скруглениях/фасках кривых поверхностей). Возвращает id операции.")]
+ public async Task LinearPattern(
+ [Description("Id операций-источников (из ранее созданных операций)")] int[] featureIds,
+ [Description("Координатная ось направления: X|Y|Z")] string axis,
+ [Description("Число экземпляров всего, включая исходный (>=2)")] int count,
+ [Description("Шаг между соседними экземплярами, мм")] double step,
+ [Description("Геометрическое копирование (быстрее)")] bool geometric = false)
+ {
+ await session.ConnectAsync();
+ var id = await modeler.LinearPatternAsync(featureIds, CoordinateAxes.Parse(axis), count, step, geometric);
+ return $"Линейный массив создан ({count} экз. вдоль {axis}, шаг {step} мм), id={id}.";
+ }
+
[McpServerTool(Name = "rebuild")]
[Description("Перестроить активный документ.")]
public async Task Rebuild()
diff --git a/tests/Kompas.Mcp.Tests/CoordinateAxisTests.cs b/tests/Kompas.Mcp.Tests/CoordinateAxisTests.cs
new file mode 100644
index 0000000..764fde4
--- /dev/null
+++ b/tests/Kompas.Mcp.Tests/CoordinateAxisTests.cs
@@ -0,0 +1,21 @@
+using Kompas.Mcp.Core.Modeling;
+
+namespace Kompas.Mcp.Tests;
+
+[Trait("Category", "Unit")]
+public sealed class CoordinateAxisTests
+{
+ [Theory]
+ [InlineData("X", CoordinateAxis.X)]
+ [InlineData("y", CoordinateAxis.Y)]
+ [InlineData(" Z ", CoordinateAxis.Z)]
+ public void Parse_maps_known(string value, CoordinateAxis expected)
+ => Assert.Equal(expected, CoordinateAxes.Parse(value));
+
+ [Theory]
+ [InlineData("W")]
+ [InlineData("")]
+ [InlineData(null)]
+ public void Parse_throws_on_unknown(string? value)
+ => Assert.Throws(() => CoordinateAxes.Parse(value!));
+}
diff --git a/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs
index e74662f..587f448 100644
--- a/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs
+++ b/tests/Kompas.Mcp.Tests/Integration/FeatureOpsTests.cs
@@ -136,6 +136,35 @@ public sealed class FeatureOpsTests
finally { await _docs.CloseAsync(save: false); }
}
+ [Fact]
+ public async Task LinearPattern_replicates_cylinder_along_X()
+ {
+ await _docs.CreateAsync(KompasDocumentType.Part);
+ try
+ {
+ // Цилиндр R5×H10 в центре (объём π·25·10 ≈ 785.4 мм³).
+ var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
+ await _modeler.AddCircleAsync(s, 0, 0, 5);
+ await _modeler.CloseSketchAsync(s);
+ var cyl = await _modeler.ExtrudeAsync(s, depth: 10);
+ var one = (await _query.GetPartInfoAsync()).Volume;
+
+ // Линейный массив: 3 экземпляра вдоль X с шагом 30 (R5 → не перекрываются).
+ var id = await _modeler.LinearPatternAsync(new[] { cyl }, CoordinateAxis.X, count: 3, step: 30);
+ Assert.True(id > 0);
+ await _modeler.RebuildAsync();
+
+ // count=3 включает исходный → суммарный объём ≈ 3 цилиндра.
+ var v = (await _query.GetPartInfoAsync()).Volume;
+ Assert.InRange(v, one * 3 * 0.95, one * 3 * 1.05);
+
+ // Габарит вдоль X: копии в 0/30/60, R5 → от −5 до 65 → размер ≈70 (знак шага не важен).
+ var box = await _query.GetBoundingBoxAsync();
+ Assert.InRange(box.SizeX, 70 * 0.95, 70 * 1.05);
+ }
+ finally { await _docs.CloseAsync(save: false); }
+ }
+
[Fact]
public async Task Loft_two_squares_makes_solid()
{
diff --git a/tests/Kompas.Mcp.Tests/SketchGeometryTests.cs b/tests/Kompas.Mcp.Tests/SketchGeometryTests.cs
index 75631c6..a5e41ef 100644
--- a/tests/Kompas.Mcp.Tests/SketchGeometryTests.cs
+++ b/tests/Kompas.Mcp.Tests/SketchGeometryTests.cs
@@ -45,6 +45,14 @@ public sealed class SketchGeometryTests
Assert.Null(Record.Exception(() => SketchGeometry.RequireVertexCount(3)));
}
+ [Fact]
+ public void RequireMin_throws_below_min()
+ {
+ Assert.Throws(() => SketchGeometry.RequireMin(1, 2, "count"));
+ Assert.Null(Record.Exception(() => SketchGeometry.RequireMin(2, 2, "count")));
+ Assert.Null(Record.Exception(() => SketchGeometry.RequireMin(5, 2, "count")));
+ }
+
[Fact]
public void RequirePoints_throws_on_null()
=> Assert.Throws(