Files
kompas3d-mcp/src/Kompas.Mcp.Core/Assemblies/AssemblyService.cs
T
mikhail 5eee7ad1af 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>
2026-05-27 13:04:19 +03:00

95 lines
5.3 KiB
C#

using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using KompasAPI7;
namespace Kompas.Mcp.Core.Assemblies;
/// <summary>
/// Операции со сборками через API7 (<c>IParts7</c>). Аналогично <c>HoleService</c>/<c>FaceEditService</c>:
/// контейнеры берутся COM-QI, все вызовы — на STA-потоке <see cref="KompasDispatcher"/>.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class AssemblyService
{
private readonly KompasSession _session;
private readonly KompasDispatcher _dispatcher;
public AssemblyService(KompasSession session, KompasDispatcher dispatcher)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
/// <summary>
/// Вставить компонент (деталь .m3d или подсборку .a3d) из файла в активную сборку, разместив
/// его origin в мировой точке (<paramref name="x"/>,<paramref name="y"/>,<paramref name="z"/>) мм.
/// </summary>
public Task<AddedComponent> AddComponentAsync(string filePath, double x, double y, double z, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => AddComponentCore(filePath, x, y, z), ct);
private AddedComponent AddComponentCore(string filePath, double x, double y, double z)
{
var path = AssemblyValidation.NormalizeComponentPath(filePath);
AssemblyValidation.RequireFinitePosition(x, y, z);
if (!File.Exists(path))
throw new FileNotFoundException($"Файл компонента не найден: {path}", path);
var top = RequireActiveAssembly();
var parts = top.Parts
?? throw new InvalidOperationException("Не удалось получить коллекцию компонентов сборки (Parts).");
// ExternalFile=true: компонент ссылается на внешний .m3d/.a3d. Redraw=true: вставить и
// перерисовать в исходном положении (само перемещение применит UpdatePlacement ниже).
var part = parts.AddFromFile(path, true, true) as IPart7
?? throw new InvalidOperationException(
$"Не удалось вставить компонент из файла (AddFromFile вернул null или не IPart7): {path}");
try
{
var placement = part.Placement as IPlacement3D
?? throw new InvalidOperationException("Placement компонента не приводится к IPlacement3D.");
if (!placement.SetOrigin(x, y, z))
throw new InvalidOperationException("SetOrigin вернул FALSE — не удалось задать положение компонента.");
// UpdatePlacement применяет изменение Placement; для вручную позиционируемого компонента
// (без сопряжений/зависимостей) он может вернуть FALSE — это НЕ ошибка (нечего
// пересчитывать). Фактически положение становится видимым в геометрии после RebuildModel.
part.UpdatePlacement(true);
top.RebuildModel(true);
return new AddedComponent { Index = parts.Count - 1, Name = SafeName(part) };
}
catch
{
// Best-effort откат осиротевшего компонента, если позиционирование не удалось.
try { part.Owner?.Delete(); } catch { /* лучшее усилие */ }
throw;
}
}
/// <summary>
/// Получить верхний компонент активной сборки, проверив тип документа. Раздельные ошибки:
/// нет документа / документ не сборка / не 3D / нет TopPart. Деталь — тоже IKompasDocument3D,
/// поэтому отсекаем по DocumentType ДО доступа к TopPart.
/// </summary>
private IPart7 RequireActiveAssembly()
{
var doc = _session.Application.ActiveDocument
?? throw new InvalidOperationException("Нет активного документа. Создайте сборку: document_create assembly.");
if (doc.DocumentType != DocumentTypeEnum.ksDocumentAssembly)
throw new InvalidOperationException(
$"Активный документ не является сборкой (тип: {doc.DocumentType}). Создайте сборку: document_create assembly.");
if (doc is not IKompasDocument3D doc3d)
throw new InvalidOperationException("Активный документ не является 3D-документом.");
return doc3d.TopPart as IPart7
?? throw new InvalidOperationException("Не удалось получить TopPart сборки.");
}
private static string SafeName(IPart7 part)
{
try { return part.Name ?? string.Empty; }
catch { return string.Empty; }
}
}