feat: assembly_add_component — вставка детали из .m3d в сборку (API7)

Веха СБОРКИ, инкремент 1. Новый AssemblyService (API7, паттерн HoleService):
IParts7.AddFromFile → Placement.SetOrigin → RebuildModel. Новый AssemblyTools
(MCP-инструмент assembly_add_component), регистрация в DI.

Ключевая находка: UpdatePlacement возвращает FALSE для вручную позиционируемого
компонента (не ошибка), позицию применяет SetOrigin + RebuildModel(true).

Валидация — чистый AssemblyValidation (расширение .m3d/.a3d, нормализация пути,
конечность координат). RequireActiveAssembly — раздельные ошибки по типу документа.

Тесты: +14 unit (AssemblyValidation), +3 integration (AssemblyTests: вставка в
origin, позиционирование со сдвигом, отклонение не-сборки). Итого 83 unit + 53
integration = 136 зелёных.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 13:04:19 +03:00
parent 5f17884f06
commit 5eee7ad1af
8 changed files with 343 additions and 1 deletions
@@ -0,0 +1,44 @@
using Kompas.Mcp.Core.Assemblies;
namespace Kompas.Mcp.Tests;
[Trait("Category", "Unit")]
public sealed class AssemblyValidationTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void NormalizeComponentPath_throws_on_empty(string? path)
=> Assert.Throws<ArgumentException>(() => AssemblyValidation.NormalizeComponentPath(path));
[Theory]
[InlineData(@"C:\parts\bolt.step")]
[InlineData(@"C:\parts\bolt.txt")]
[InlineData(@"C:\parts\bolt")]
public void NormalizeComponentPath_throws_on_wrong_extension(string path)
=> Assert.Throws<ArgumentException>(() => AssemblyValidation.NormalizeComponentPath(path));
[Theory]
[InlineData(@"C:\parts\bolt.m3d", ".m3d")]
[InlineData(@"C:\parts\frame.a3d", ".a3d")]
[InlineData(@"C:\parts\bolt.M3D", ".m3d")] // регистр не важен
[InlineData(" C:\\parts\\bolt.a3d ", ".a3d")] // пробелы обрезаются
public void NormalizeComponentPath_accepts_m3d_and_a3d(string path, string expectedExt)
{
var result = AssemblyValidation.NormalizeComponentPath(path);
Assert.True(Path.IsPathFullyQualified(result), "Должен вернуться абсолютный путь.");
Assert.EndsWith(expectedExt, result, StringComparison.OrdinalIgnoreCase);
}
[Theory]
[InlineData(double.NaN, 0, 0)]
[InlineData(0, double.PositiveInfinity, 0)]
[InlineData(0, 0, double.NegativeInfinity)]
public void RequireFinitePosition_throws_on_non_finite(double x, double y, double z)
=> Assert.Throws<ArgumentException>(() => AssemblyValidation.RequireFinitePosition(x, y, z));
[Fact]
public void RequireFinitePosition_accepts_finite()
=> Assert.Null(Record.Exception(() => AssemblyValidation.RequireFinitePosition(50, -10, 0)));
}