feat: параметрика — переменные модели (create/set/delete_variable) через API5

- VariableService: CreateVariableAsync/SetVariableAsync/DeleteVariableAsync/GetVariableValueAsync
- создание ТОЛЬКО на ksFeature.VariableCollection (ksPart — только внешние); RebuildModel + re-fetch
- Expression — ведущее поле (константа/формула со ссылками); value пересчитывается
- list_variables (ModelInspectionService) исправлен: читает feature-коллекцию (все переменные)
- инструменты create_variable/set_variable/delete_variable (VariableTools) + DI
- интеграционный тест CRUD round-trip (создание, формула width*2, удаление по зависимостям)
- ОГРАНИЧЕНИЕ: переменные драйвят геометрию лишь в параметрической модели (эскизы на литералах — нет)
- 116 тестов; спек docs/superpowers/specs/2026-05-27-parametrics-design.md
This commit is contained in:
2026-05-27 11:14:28 +03:00
parent 7d85eb949b
commit bc52b5d920
6 changed files with 298 additions and 1 deletions
@@ -0,0 +1,111 @@
using System.Globalization;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using Kompas6Constants3D;
namespace Kompas.Mcp.Core.Modeling;
/// <summary>
/// Параметрика: создание и изменение переменных модели (API5 <c>ksVariable</c>).
/// Создание возможно только на коллекции переменных корневого <c>ksFeature</c> (не <c>ksPart</c>).
/// <para>ВАЖНО: переменная управляет геометрией лишь в параметрической модели (где размеры
/// эскизов связаны с именованными переменными). Эскизы, построенные литеральными координатами,
/// не имеют именованных размеров — для них переменные хранятся, но геометрию не меняют.</para>
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class VariableService
{
private readonly KompasSession _session;
private readonly KompasDispatcher _dispatcher;
public VariableService(KompasSession session, KompasDispatcher dispatcher)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
/// <summary>Создать новую переменную модели с начальным значением. external=true — внешняя
/// (доступна для управления из сборки). Возвращает вычисленное значение.</summary>
public Task<double> CreateVariableAsync(string name, double value, string? note = null, bool external = false, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
RequireName(name);
if (!double.IsFinite(value)) throw new ArgumentException("Значение должно быть конечным.", nameof(value));
var (part, vc) = RootVariables();
if (vc.GetByName(name, true, false) is ksVariable)
throw new InvalidOperationException($"Переменная '{name}' уже существует.");
var v = vc.AddNewVariable(name, value, note ?? "") as ksVariable
?? throw new InvalidOperationException("AddNewVariable вернул null (недопустимое имя?).");
if (external) v.external = true;
part.RebuildModel();
return ReadValueFresh(name); // значение перечитываем после перестроения
}, ct);
/// <summary>Задать выражение переменной (константа «30» или формула «len*2+5») и перестроить.
/// Возвращает вычисленное значение.</summary>
public Task<double> SetVariableAsync(string name, string expression, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
RequireName(name);
if (string.IsNullOrWhiteSpace(expression))
throw new ArgumentException("Выражение не может быть пустым.", nameof(expression));
var (part, vc) = RootVariables();
var v = RequireVariable(vc, name);
v.Expression = expression;
part.RebuildModel();
return ReadValueFresh(name); // переменную перечитываем после перестроения (value пересчитан)
}, ct);
/// <summary>Удалить переменную модели.</summary>
public Task DeleteVariableAsync(string name, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
RequireName(name);
var (part, vc) = RootVariables();
RequireVariable(vc, name); // понятная ошибка, если её нет
if (!vc.RemoveVariable(name))
throw new InvalidOperationException($"Не удалось удалить переменную '{name}'.");
part.RebuildModel();
}, ct);
/// <summary>Прочитать текущее (вычисленное) значение переменной.</summary>
public Task<double> GetVariableValueAsync(string name, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
RequireName(name);
var (_, vc) = RootVariables();
return RequireVariable(vc, name).value;
}, ct);
/// <summary>Перечитать значение переменной из свежей коллекции (после RebuildModel оно пересчитано).</summary>
private double ReadValueFresh(string name)
{
var (_, vc) = RootVariables();
return RequireVariable(vc, name).value;
}
private static void RequireName(string name)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Имя переменной не может быть пустым.", nameof(name));
}
private static ksVariable RequireVariable(ksVariableCollection vc, string name)
=> vc.GetByName(name, true, false) as ksVariable
?? throw new KeyNotFoundException($"Переменная '{name}' не найдена.");
/// <summary>Корневой ksFeature детали и его коллекция переменных (на ней работает AddNewVariable).</summary>
private (ksPart part, ksVariableCollection vc) RootVariables()
{
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
?? throw new InvalidOperationException("Нет активного 3D-документа.");
var part = doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
var feature = part.GetFeature() as ksFeature
?? throw new InvalidOperationException("GetFeature() вернул null (нет дерева построения).");
var vc = feature.VariableCollection as ksVariableCollection
?? throw new InvalidOperationException("VariableCollection вернул не ksVariableCollection.");
return (part, vc);
}
}
@@ -260,7 +260,11 @@ public sealed class ModelInspectionService
private static IReadOnlyList<VariableInfo> ReadVariables(ksPart part)
{
if (part.VariableCollection() is not ksVariableCollection vc) return Array.Empty<VariableInfo>();
// Переменные детали живут в коллекции корневого ksFeature; ksPart.VariableCollection() даёт
// только внешние. Берём feature-коллекцию (все переменные, включая созданные), с фолбэком.
var vc = (part.GetFeature() as ksFeature)?.VariableCollection as ksVariableCollection
?? part.VariableCollection() as ksVariableCollection;
if (vc is null) return Array.Empty<VariableInfo>();
int n = vc.GetCount();
var list = new List<VariableInfo>(n);
for (int i = 0; i < n; i++)
+1
View File
@@ -23,6 +23,7 @@ builder.Services.AddSingleton<KompasSession>();
builder.Services.AddSingleton<DocumentService>();
builder.Services.AddSingleton<PartModeler>();
builder.Services.AddSingleton<HoleService>();
builder.Services.AddSingleton<VariableService>();
builder.Services.AddSingleton<SnapshotService>();
builder.Services.AddSingleton<QueryService>();
builder.Services.AddSingleton<ModelInspectionService>();
@@ -0,0 +1,44 @@
using System.ComponentModel;
using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Modeling;
using ModelContextProtocol.Server;
namespace Kompas.Mcp.Host.Tools;
/// <summary>Инструменты параметрики: переменные модели (создание, изменение, удаление).</summary>
[McpServerToolType]
public sealed class VariableTools(KompasSession session, VariableService variables)
{
[McpServerTool(Name = "create_variable")]
[Description("Создать переменную модели с начальным числовым значением value. note — комментарий. external=true — внешняя (доступна для управления из сборки). Имя должно быть уникальным. Примечание: переменная управляет геометрией только в параметрической модели (где размеры эскизов связаны с именами переменных); для моделей с литеральными размерами переменная просто хранится. Используйте list_variables для просмотра. Возвращает вычисленное значение.")]
public async Task<string> CreateVariable(
string name,
[Description("Начальное числовое значение")] double value,
[Description("Комментарий (необязательно)")] string? note = null,
[Description("Внешняя переменная (управляема из сборки)")] bool external = false)
{
await session.ConnectAsync();
var v = await variables.CreateVariableAsync(name, value, note, external);
return $"Переменная '{name}' создана (значение {v}).";
}
[McpServerTool(Name = "set_variable")]
[Description("Задать выражение существующей переменной: константа (например «30») или формула со ссылками на другие переменные (например «width*2+5»). Модель перестраивается, значение пересчитывается. Возвращает вычисленное значение.")]
public async Task<string> SetVariable(
string name,
[Description("Выражение: число или формула (width*2+5)")] string expression)
{
await session.ConnectAsync();
var v = await variables.SetVariableAsync(name, expression);
return $"Переменная '{name}' = «{expression}» → {v}.";
}
[McpServerTool(Name = "delete_variable")]
[Description("Удалить переменную модели по имени. Нельзя удалить переменную, на которую ссылаются другие (сначала удалите зависимые).")]
public async Task<string> DeleteVariable(string name)
{
await session.ConnectAsync();
await variables.DeleteVariableAsync(name);
return $"Переменная '{name}' удалена.";
}
}