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(