feat: линейный массив (linear_pattern) — ksMeshCopyDefinition o3d_meshCopy
- CoordinateAxis enum + CoordinateAxes.ToObj3dType/Parse (X/Y/Z → o3d_axisO*) - SketchGeometry.RequireMin; PartModeler.RequireFeature (источник по id из _features) - LinearPatternAsync: SetAxis1 + SetCopyParamAlongAxis(true,0,count,step,false), count2=1 - инструмент linear_pattern; unit-тесты (RequireMin, Parse), интеграционный тест (3 экз. ±5%) - эмпирически: count включает исходный, step мм между соседними, count2=1 выключает 2D
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
using Kompas6Constants3D;
|
||||||
|
|
||||||
|
namespace Kompas.Mcp.Core.Modeling;
|
||||||
|
|
||||||
|
/// <summary>Базовая координатная ось (для направления массива / оси вращения).</summary>
|
||||||
|
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)),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -331,6 +331,46 @@ public sealed partial class PartModeler
|
|||||||
return id;
|
return id;
|
||||||
}, ct);
|
}, ct);
|
||||||
|
|
||||||
|
/// <summary>Линейный массив (по сетке вдоль одной координатной оси): размножить операции
|
||||||
|
/// featureIds вдоль оси axis, count экземпляров всего (включая исходный, ≥2), шаг step (мм)
|
||||||
|
/// между соседними. geometric=true — быстрое геометрическое копирование. Возвращает id операции.</summary>
|
||||||
|
public Task<int> LinearPatternAsync(IReadOnlyList<int> 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);
|
||||||
|
|
||||||
/// <summary>Перестроить документ.</summary>
|
/// <summary>Перестроить документ.</summary>
|
||||||
public Task RebuildAsync(CancellationToken ct = default)
|
public Task RebuildAsync(CancellationToken ct = default)
|
||||||
=> _dispatcher.InvokeAsync(() =>
|
=> _dispatcher.InvokeAsync(() =>
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ public sealed partial class PartModeler : IDisposable
|
|||||||
=> _sketches.TryGetValue(id, out var s) ? s
|
=> _sketches.TryGetValue(id, out var s) ? s
|
||||||
: throw new KeyNotFoundException($"Эскиз с id={id} не найден.");
|
: throw new KeyNotFoundException($"Эскиз с id={id} не найден.");
|
||||||
|
|
||||||
|
/// <summary>Получить ksEntity ранее созданной операции по её id (источник для массива/зеркала).</summary>
|
||||||
|
private ksEntity RequireFeature(int id)
|
||||||
|
=> _features.TryGetValue(id, out var f) ? f
|
||||||
|
: throw new KeyNotFoundException($"Операция с id={id} не найдена.");
|
||||||
|
|
||||||
private SketchEntry RequireOpenSketch(int id)
|
private SketchEntry RequireOpenSketch(int id)
|
||||||
{
|
{
|
||||||
var s = RequireSketch(id);
|
var s = RequireSketch(id);
|
||||||
|
|||||||
@@ -34,6 +34,13 @@ public static class SketchGeometry
|
|||||||
throw new ArgumentOutOfRangeException(nameof(count), count, "Число вершин должно быть >= 3.");
|
throw new ArgumentOutOfRangeException(nameof(count), count, "Число вершин должно быть >= 3.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Требовать целое значение не меньше <paramref name="min"/> (например, число экземпляров массива).</summary>
|
||||||
|
public static void RequireMin(int value, int min, string paramName)
|
||||||
|
{
|
||||||
|
if (value < min)
|
||||||
|
throw new ArgumentOutOfRangeException(paramName, value, $"Значение должно быть >= {min}.");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Валидировать список точек: не null, минимум <paramref name="min"/>, все координаты конечны.</summary>
|
/// <summary>Валидировать список точек: не null, минимум <paramref name="min"/>, все координаты конечны.</summary>
|
||||||
public static void RequirePoints(IReadOnlyList<(double x, double y)> points, int min, string paramName)
|
public static void RequirePoints(IReadOnlyList<(double x, double y)> points, int min, string paramName)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -134,6 +134,20 @@ public sealed class FeatureTools(KompasSession session, PartModeler modeler)
|
|||||||
return $"Операция по сечениям создана ({sections} сечений), id={id}.";
|
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<string> 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")]
|
[McpServerTool(Name = "rebuild")]
|
||||||
[Description("Перестроить активный документ.")]
|
[Description("Перестроить активный документ.")]
|
||||||
public async Task<string> Rebuild()
|
public async Task<string> Rebuild()
|
||||||
|
|||||||
@@ -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<ArgumentException>(() => CoordinateAxes.Parse(value!));
|
||||||
|
}
|
||||||
@@ -136,6 +136,35 @@ public sealed class FeatureOpsTests
|
|||||||
finally { await _docs.CloseAsync(save: false); }
|
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]
|
[Fact]
|
||||||
public async Task Loft_two_squares_makes_solid()
|
public async Task Loft_two_squares_makes_solid()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ public sealed class SketchGeometryTests
|
|||||||
Assert.Null(Record.Exception(() => SketchGeometry.RequireVertexCount(3)));
|
Assert.Null(Record.Exception(() => SketchGeometry.RequireVertexCount(3)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RequireMin_throws_below_min()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => SketchGeometry.RequireMin(1, 2, "count"));
|
||||||
|
Assert.Null(Record.Exception(() => SketchGeometry.RequireMin(2, 2, "count")));
|
||||||
|
Assert.Null(Record.Exception(() => SketchGeometry.RequireMin(5, 2, "count")));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RequirePoints_throws_on_null()
|
public void RequirePoints_throws_on_null()
|
||||||
=> Assert.Throws<ArgumentNullException>(
|
=> Assert.Throws<ArgumentNullException>(
|
||||||
|
|||||||
Reference in New Issue
Block a user