8282716773
Веха СБОРКИ, инкремент 2. AssemblyService.AddMateAsync (API7): top.MateConstraints
→ Add(MateConstraintType) → BaseObject1/2 (грани через FindObjectsByPoint
FirstLevel=false) → ParamValue → Update → RebuildModel. Свой enum MateType
(Coincidence/Distance) → MateConstraintType. MCP-инструмент assembly_add_mate.
Подтверждено спайком и тестами: coincidence сводит грани заподлицо (ширина 120→40),
distance(30) держит зазор 30 (→70); Update=True Valid=True. Решатель может двигать
любой компонент → проверка по ширине габарита.
Ревью Codex учтено: scope урезан до coincidence/distance (только геометрически
проверенные); валидация value по типу (RequireMateValue); полный откат при любой
ошибке после Add (mate.Owner.Delete); возврат AddedMate{Type,Valid}; усиленные
ассерты (Valid, размеры компонентов неизменны). Точечный release RCW отклонён
(конвенция v2-2). Прочие типы (parallel/concentric/angle/…) — будущие инкременты.
Тесты: +14 unit (Mates.Parse/RequiresValue, RequireMateValue), +2 integration
(AssemblyMateTests). Итого 97 unit + 57 integration = 154 зелёных.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
200 lines
12 KiB
C#
200 lines
12 KiB
C#
using System.Runtime.Versioning;
|
||
using Kompas.Mcp.Core.Threading;
|
||
using Kompas6Constants;
|
||
using Kompas6Constants3D;
|
||
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).");
|
||
|
||
// Фиксируем число компонентов ДО вставки — для верифицируемого отката (откатываем всё, что
|
||
// появилось сверх beforeCount), не полагаясь на возможный null-возврат AddFromFile.
|
||
int beforeCount = parts.Count;
|
||
|
||
// ExternalFile=true: компонент ссылается на внешний .m3d/.a3d. Redraw=true: вставить и
|
||
// перерисовать в исходном положении (само перемещение применит SetOrigin+RebuildModel ниже).
|
||
var part = parts.AddFromFile(path, true, true) as IPart7;
|
||
if (part is null)
|
||
{
|
||
// AddFromFile мог вставить компонент и всё равно вернуть null/не-IPart7 — почистим, если
|
||
// число компонентов выросло, чтобы не оставить «осиротевший» компонент в сборке.
|
||
RollbackInsertion(top, parts, beforeCount);
|
||
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);
|
||
if (!top.RebuildModel(true))
|
||
throw new InvalidOperationException("RebuildModel вернул FALSE — сборка не перестроена после вставки компонента.");
|
||
|
||
return new AddedComponent { Index = parts.Count - 1, Name = SafeName(part) };
|
||
}
|
||
catch
|
||
{
|
||
// Верифицируемый откол: удаляем всё, что добавилось сверх beforeCount, и проверяем.
|
||
RollbackInsertion(top, parts, beforeCount);
|
||
throw;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Наложить сопряжение <paramref name="type"/> между гранями двух компонентов активной сборки,
|
||
/// выбранными по мировым точкам (<paramref name="x1"/>,<paramref name="y1"/>,<paramref name="z1"/>)
|
||
/// и (<paramref name="x2"/>,<paramref name="y2"/>,<paramref name="z2"/>). Для расстояния
|
||
/// <paramref name="value"/> — зазор (мм). Решатель спозиционирует компоненты; результат — статус
|
||
/// построенного сопряжения.
|
||
/// </summary>
|
||
public Task<AddedMate> AddMateAsync(MateType type, double x1, double y1, double z1,
|
||
double x2, double y2, double z2, double value, CancellationToken ct = default)
|
||
=> _dispatcher.InvokeAsync(() => AddMateCore(type, x1, y1, z1, x2, y2, z2, value), ct);
|
||
|
||
private AddedMate AddMateCore(MateType type, double x1, double y1, double z1,
|
||
double x2, double y2, double z2, double value)
|
||
{
|
||
AssemblyValidation.RequireFinitePosition(x1, y1, z1);
|
||
AssemblyValidation.RequireFinitePosition(x2, y2, z2);
|
||
AssemblyValidation.RequireMateValue(Mates.RequiresValue(type), value);
|
||
|
||
var top = RequireActiveAssembly();
|
||
var face1 = FindFaceInAssembly(top, x1, y1, z1)
|
||
?? throw new InvalidOperationException(
|
||
$"В точке ({x1},{y1},{z1}) не найдена грань компонента. Укажите точку в середине грани (model_snapshot).");
|
||
var face2 = FindFaceInAssembly(top, x2, y2, z2)
|
||
?? throw new InvalidOperationException(
|
||
$"В точке ({x2},{y2},{z2}) не найдена грань компонента. Укажите точку в середине грани (model_snapshot).");
|
||
|
||
var mates = top.MateConstraints
|
||
?? throw new InvalidOperationException("Не удалось получить коллекцию сопряжений сборки (MateConstraints).");
|
||
var mate = mates.Add(Mates.ToConstraintType(type))
|
||
?? throw new InvalidOperationException("MateConstraints.Add вернул null.");
|
||
|
||
try
|
||
{
|
||
mate.BaseObject1 = (IModelObject)face1;
|
||
mate.BaseObject2 = (IModelObject)face2;
|
||
if (Mates.RequiresValue(type))
|
||
mate.ParamValue = value;
|
||
|
||
if (!mate.Update())
|
||
throw new InvalidOperationException(
|
||
"Update сопряжения вернул FALSE — связь не наложена (несовместимые грани/значение?).");
|
||
if (!top.RebuildModel(true))
|
||
throw new InvalidOperationException("RebuildModel вернул FALSE — сборка не перестроена после сопряжения.");
|
||
|
||
return new AddedMate { Type = type, Valid = mate.Valid };
|
||
}
|
||
catch
|
||
{
|
||
// Полный откат: при ЛЮБОЙ ошибке после Add удаляем сопряжение, чтобы не оставить «сироту».
|
||
try { mate.Owner?.Delete(); top.RebuildModel(true); } catch { /* лучшее усилие */ }
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private static IFace? FindFaceInAssembly(IPart7 top, double x, double y, double z)
|
||
{
|
||
// FirstLevel=false — спускаемся в компоненты, чтобы получить их грани в контексте сборки.
|
||
var hit = top.FindObjectsByPoint(x, y, z, false);
|
||
switch (hit)
|
||
{
|
||
case IFace f: return f;
|
||
case object[] arr:
|
||
foreach (var o in arr) if (o is IFace face) return face;
|
||
return null;
|
||
case System.Collections.IEnumerable en:
|
||
foreach (var o in en) if (o is IFace face) return face;
|
||
return null;
|
||
default: return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Откатить вставку: удалить компоненты, появившиеся сверх <paramref name="beforeCount"/> (обычно
|
||
/// один — только что вставленный). Best-effort, но с верификацией: компонент удаляется через
|
||
/// <c>IFeature7.Delete()</c> своего владельца, затем сборка перестраивается.
|
||
/// </summary>
|
||
private static void RollbackInsertion(IPart7 top, IParts7 parts, int beforeCount)
|
||
{
|
||
try
|
||
{
|
||
// Удаляем с конца, пока число не вернётся к исходному (защита от частичной вставки).
|
||
for (int guard = 0; parts.Count > beforeCount && guard < 8; guard++)
|
||
{
|
||
if (parts.Part[parts.Count - 1] is IPart7 last && last.Owner is { } owner)
|
||
owner.Delete();
|
||
else
|
||
break; // не удалось получить владельца — выходим, чтобы не зациклиться
|
||
}
|
||
top.RebuildModel(true);
|
||
}
|
||
catch { /* лучшее усилие: исходное исключение важнее ошибки отката */ }
|
||
}
|
||
|
||
/// <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; }
|
||
}
|
||
}
|