feat: assembly_add_mate — сопряжения компонентов сборки (API7)

Веха СБОРКИ, инкремент 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>
This commit is contained in:
2026-05-27 13:39:04 +03:00
parent 8afad88241
commit 8282716773
8 changed files with 324 additions and 3 deletions
@@ -0,0 +1,11 @@
namespace Kompas.Mcp.Core.Assemblies;
/// <summary>Результат наложения сопряжения между компонентами сборки.</summary>
public sealed record AddedMate
{
/// <summary>Тип наложенного сопряжения (как был запрошен).</summary>
public required MateType Type { get; init; }
/// <summary>true — сопряжение успешно построено и не вырождено после перестроения.</summary>
public required bool Valid { get; init; }
}
@@ -1,6 +1,7 @@
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using Kompas6Constants3D;
using KompasAPI7;
namespace Kompas.Mcp.Core.Assemblies;
@@ -79,6 +80,77 @@ public sealed class AssemblyService
}
}
/// <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, но с верификацией: компонент удаляется через
@@ -29,10 +29,22 @@ public static class AssemblyValidation
return Path.GetFullPath(trimmed);
}
/// <summary>Проверить, что координаты вставки конечны (отсекает NaN/Infinity).</summary>
/// <summary>Проверить, что координаты конечны (отсекает NaN/Infinity).</summary>
public static void RequireFinitePosition(double x, double y, double z)
{
if (!double.IsFinite(x) || !double.IsFinite(y) || !double.IsFinite(z))
throw new ArgumentException("Координаты вставки компонента должны быть конечными числами (не NaN/Infinity).");
throw new ArgumentException("Координаты должны быть конечными числами (не NaN/Infinity).");
}
/// <summary>
/// Проверить числовое значение сопряжения. Если значение требуется (<paramref name="requiresValue"/>,
/// напр. расстояние) — оно должно быть конечным и неотрицательным.
/// </summary>
public static void RequireMateValue(bool requiresValue, double value)
{
if (!requiresValue) return; // для типов без параметра значение игнорируется
if (!double.IsFinite(value) || value < 0)
throw new ArgumentOutOfRangeException(nameof(value),
"Значение сопряжения (расстояние) должно быть конечным и неотрицательным.");
}
}
@@ -0,0 +1,44 @@
using Kompas6Constants3D;
namespace Kompas.Mcp.Core.Assemblies;
/// <summary>
/// Дружелюбный тип сопряжения (обёртка над <see cref="MateConstraintType"/>). Пока поддержаны только
/// геометрически проверенные типы — совпадение и расстояние; остальные (параллельность, соосность,
/// угол и т.д.) добавятся в следующих инкрементах вместе с тестами.
/// </summary>
public enum MateType
{
/// <summary>Совпадение (грани лежат на одной плоскости, заподлицо).</summary>
Coincidence,
/// <summary>На расстоянии (постоянный зазор между гранями, мм).</summary>
Distance,
}
/// <summary>Маппинг дружелюбного типа сопряжения ↔ перечисление API КОМПАС и разбор строки.</summary>
public static class Mates
{
/// <summary>Сопряжения, которым нужно числовое значение (<c>ParamValue</c>): расстояние.</summary>
public static bool RequiresValue(MateType type) => type is MateType.Distance;
public static MateConstraintType ToConstraintType(MateType type) => type switch
{
MateType.Coincidence => MateConstraintType.mc_Coincidence,
MateType.Distance => MateConstraintType.mc_Distance,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "Неизвестный тип сопряжения."),
};
/// <summary>Разобрать строку типа сопряжения (англ./рус.) в <see cref="MateType"/>.</summary>
public static MateType Parse(string value)
{
ArgumentNullException.ThrowIfNull(value);
return value.Trim().ToLowerInvariant() switch
{
"coincidence" or "coincident" or "совпадение" => MateType.Coincidence,
"distance" or "расстояние" => MateType.Distance,
_ => throw new ArgumentException(
$"Неизвестный/неподдерживаемый тип сопряжения '{value}'. Сейчас поддержаны: " +
"coincidence (совпадение), distance (расстояние).", nameof(value)),
};
}
}
@@ -24,4 +24,26 @@ public sealed class AssemblyTools(KompasSession session, AssemblyService assembl
var c = await assembly.AddComponentAsync(filePath, x, y, z);
return $"Компонент '{c.Name}' вставлен в сборку, индекс {c.Index}, позиция ({x}, {y}, {z}).";
}
[McpServerTool(Name = "assembly_add_mate")]
[Description("Наложить сопряжение между гранями ДВУХ компонентов активной сборки, выбранными по " +
"мировым точкам (x1,y1,z1) и (x2,y2,z2) — точки в СЕРЕДИНЕ граней (мм). mateType: " +
"'coincidence' (совпадение, грани заподлицо) или 'distance' (на расстоянии value мм между гранями). " +
"Решатель спозиционирует компоненты. Сделайте model_snapshot/describe_face, прикиньте точки на гранях.")]
public async Task<string> AddMate(
[Description("Тип сопряжения: coincidence | distance")] string mateType,
[Description("X точки на грани компонента 1, мм")] double x1,
[Description("Y точки на грани компонента 1, мм")] double y1,
[Description("Z точки на грани компонента 1, мм")] double z1,
[Description("X точки на грани компонента 2, мм")] double x2,
[Description("Y точки на грани компонента 2, мм")] double y2,
[Description("Z точки на грани компонента 2, мм")] double z2,
[Description("Зазор для distance, мм (для coincidence не нужен)")] double value = 0)
{
await session.ConnectAsync();
var type = Mates.Parse(mateType);
var m = await assembly.AddMateAsync(type, x1, y1, z1, x2, y2, z2, value);
var valueText = Mates.RequiresValue(type) ? $", значение {value} мм" : "";
return $"Сопряжение '{type}'{valueText} наложено (valid={m.Valid}).";
}
}