feat(query): инструмент get_part_info (МЦХ детали)

QueryService через IMassInertiaParam7: объём, масса, площадь, центр масс.
Единицы выставляются детерминированно (мм/кг) через LengthUnits/MassUnits —
иначе КОМПАС отдаёт значения в единицах отображения МЦХ (по умолчанию см/г).
Calculate() возвращает FALSE даже при успехе → опираемся на свойство Actual.

Интеграционный тест: цилиндр R10×H20 → V≈6283 мм³, S≈1885 мм², m≈0.049 кг, Zc=10 мм.
Находка задокументирована в OPEN_QUESTIONS; презентация обновлена (19 инструментов, 14 тестов).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@
This commit is contained in:
2026-05-26 09:35:57 +03:00
parent 60cf26f739
commit b73748f84b
7 changed files with 172 additions and 5 deletions
+10
View File
@@ -69,6 +69,16 @@ stateful-сессии? Пока полагаемся на последовате
передавать через `ImageContentBlock.FromBytes(bytes, mimeType)` — он сам кодирует. Прямое
присваивание `Data = rawBytes` портит изображение на проводе. Исправлено в VisionTools.
### ✅ МЦХ (get_part_info): единицы измерения и поведение Calculate()
`IMassInertiaParam7` по умолчанию возвращает значения в **единицах отображения МЦХ** документа
(по умолчанию см³/см²/г, плотность г/см³) — то есть зависит от настроек, а не фиксированные СИ.
Поэтому `QueryService` принудительно выставляет `LengthUnits = ksLUnMM` и `MassUnits = ksMUnKG`
перед чтением → детерминированные **мм³, мм², кг, мм**. Проверено тестом (цилиндр R10×H20:
V≈6283 мм³, S≈1885 мм², m≈0.0494 кг, Zc=10 мм).
Тонкость: `Calculate()` возвращает **FALSE даже при успехе** (когда расчёт уже актуален после
`RebuildDocument`), поэтому опираемся на свойство `Actual`, а не на возвращаемое значение.
Материал по умолчанию — сталь (ρ≈7.856 г/см³); масса зависит от назначенного материала.
### ✅ Снимок 3D: рендер через файл, а не resultArrayBytes
`ksDocument3D.SaveAsToRasterFormat(fileName, param)` при **непустом** `fileName` возвращает TRUE,
пишет корректный PNG на диск, но `param.resultArrayBytes` остаётся **пустым**. Поэтому
+6 -5
View File
@@ -125,7 +125,7 @@
<!-- HERO -->
<header class="hero">
<div class="wrap">
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v1 работает — цикл эскиз→выдавливание→снимок через MCP (18 инструментов)</span>
<span class="badge"><span class="dot" style="background:var(--green);box-shadow:0 0 10px var(--green)"></span> Статус: v1 работает — цикл эскиз→выдавливание→снимок через MCP (19 инструментов)</span>
<h1>КОМПАС-3D <span class="g">MCP-сервер</span><br>управление CAD языком LLM</h1>
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
@@ -246,8 +246,8 @@
впереди.</p>
<div class="prog">
<div class="progrow"><span>Общий прогресс</span><span>v1 работает · 12 тестов зелёные · доки и ревью</span></div>
<div class="bar"><i style="width:72%"></i></div>
<div class="progrow"><span>Общий прогресс</span><span>v1 работает · 14 тестов зелёные · доки и ревью</span></div>
<div class="bar"><i style="width:74%"></i></div>
</div>
<div class="todo-grid" style="margin-top:26px">
@@ -282,14 +282,15 @@
<!-- TOOLS -->
<div class="panel">
<h3>Инструменты v1 <span class="tag done">ГОТОВО</span></h3>
<p class="meta">18 инструментов, отдаются по MCP-протоколу</p>
<p class="meta">19 инструментов, отдаются по MCP-протоколу</p>
<ul class="list">
<li class="muted"><span class="mark ok"></span><span class="t"><code>System</code>: connect · status · set_visible</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Documents</code>: create · open · save · save_as · close · active</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Sketch</code>: create · add line/circle/rectangle · close</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Features</code>: extrude_boss · extrude_cut · rebuild</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Vision</code>: model_snapshot 👁️</span></li>
<li><span class="mark todo"></span><span><code>Query</code>: select_face · list_bodies · get_part_info (далее)</span></li>
<li class="muted"><span class="mark ok"></span><span class="t"><code>Query</code>: get_part_info (объём/масса/площадь/центр масс)</span></li>
<li><span class="mark todo"></span><span><code>Query</code>: select_face · list_bodies (далее)</span></li>
</ul>
</div>
+12
View File
@@ -0,0 +1,12 @@
namespace Kompas.Mcp.Core.Query;
/// <summary>Массово-центровочные характеристики детали (в единицах документа, по умолчанию мм).</summary>
public sealed record PartInfo
{
public required double Volume { get; init; } // объём, мм³
public required double Mass { get; init; } // масса, кг (по материалу/плотности)
public required double Area { get; init; } // площадь поверхности, мм²
public required double CenterX { get; init; } // центр масс, X
public required double CenterY { get; init; }
public required double CenterZ { get; init; }
}
+61
View File
@@ -0,0 +1,61 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using KompasAPI7;
namespace Kompas.Mcp.Core.Query;
/// <summary>Запросы к активной 3D-модели: массово-центровочные характеристики.</summary>
[SupportedOSPlatform("windows")]
public sealed class QueryService
{
private readonly KompasSession _session;
private readonly KompasDispatcher _dispatcher;
public QueryService(KompasSession session, KompasDispatcher dispatcher)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
/// <summary>Рассчитать и вернуть МЦХ активной детали (объём, масса, площадь, центр масс).</summary>
public Task<PartInfo> GetPartInfoAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var doc3d = _session.Application.ActiveDocument as IKompasDocument3D
?? throw new InvalidOperationException("Активный документ не является 3D-моделью.");
var part = doc3d.TopPart
?? throw new InvalidOperationException("Не удалось получить вершинный компонент.");
try
{
var mass = (IMassInertiaParam7)part;
// Детерминированные единицы: длина — мм, масса — кг. Иначе КОМПАС вернёт
// значения в единицах отображения МЦХ (по умолчанию см/г), что зависит от настроек.
mass.LengthUnits = ksLengthUnitsEnum.ksLUnMM;
mass.MassUnits = ksMassUnitsEnum.ksMUnKG;
// Calculate() возвращает FALSE и при успехе (когда расчёт уже актуален),
// поэтому ориентируемся на свойство Actual, а не на возвращаемое значение.
mass.Calculate();
if (!mass.Actual)
throw new InvalidOperationException("МЦХ не рассчитаны (модель пуста или не перестроена?).");
return new PartInfo
{
Volume = mass.Volume,
Mass = mass.Mass,
Area = mass.Area,
CenterX = mass.Xc,
CenterY = mass.Yc,
CenterZ = mass.Zc,
};
}
finally
{
// part и его QI-приведение mass — один RCW; release один раз.
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
}
}, ct);
}
+2
View File
@@ -1,6 +1,7 @@
using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Query;
using Kompas.Mcp.Core.Threading;
using Kompas.Mcp.Core.Vision;
using Microsoft.Extensions.DependencyInjection;
@@ -19,6 +20,7 @@ builder.Services.AddSingleton<KompasSession>();
builder.Services.AddSingleton<DocumentService>();
builder.Services.AddSingleton<PartModeler>();
builder.Services.AddSingleton<SnapshotService>();
builder.Services.AddSingleton<QueryService>();
// MCP-сервер поверх stdio; инструменты находятся по атрибутам в этой сборке.
builder.Services
+21
View File
@@ -0,0 +1,21 @@
using System.ComponentModel;
using Kompas.Mcp.Core;
using Kompas.Mcp.Core.Query;
using ModelContextProtocol.Server;
namespace Kompas.Mcp.Host.Tools;
/// <summary>Инструменты запросов к модели.</summary>
[McpServerToolType]
public sealed class QueryTools(KompasSession session, QueryService query)
{
[McpServerTool(Name = "get_part_info")]
[Description("Массово-центровочные характеристики активной детали: объём (мм³), масса (кг), площадь (мм²) и центр масс. Полезно для числовой проверки построения.")]
public async Task<string> GetPartInfo()
{
await session.ConnectAsync();
var i = await query.GetPartInfoAsync();
return $"Объём: {i.Volume:F2} мм³; масса: {i.Mass:F4} кг; площадь: {i.Area:F2} мм²; " +
$"центр масс: ({i.CenterX:F2}, {i.CenterY:F2}, {i.CenterZ:F2}).";
}
}
@@ -0,0 +1,60 @@
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Modeling;
using Kompas.Mcp.Core.Query;
namespace Kompas.Mcp.Tests.Integration;
/// <summary>Интеграция: массово-центровочные характеристики (get_part_info).</summary>
[Trait("Category", "Integration")]
[Collection(KompasCollection.Name)]
public sealed class QueryTests
{
private readonly DocumentService _docs;
private readonly PartModeler _modeler;
private readonly QueryService _query;
public QueryTests(KompasFixture fx)
{
_docs = new DocumentService(fx.Session, fx.Dispatcher);
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
_query = new QueryService(fx.Session, fx.Dispatcher);
}
[Fact]
public async Task Cylinder_volume_matches_formula()
{
await _docs.CreateAsync(KompasDocumentType.Part);
try
{
// Цилиндр R10 × H20 на XOY: объём = π·R²·H = π·100·20 ≈ 6283.19 мм³.
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
await _modeler.AddCircleAsync(s, 0, 0, 10);
await _modeler.CloseSketchAsync(s);
var boss = await _modeler.ExtrudeAsync(s, depth: 20);
Assert.True(boss > 0);
await _modeler.RebuildAsync();
var info = await _query.GetPartInfoAsync();
// Единицы детерминированы сервисом: мм³, мм², кг, мм.
var expectedVolume = Math.PI * 100 * 20; // ≈ 6283.19 мм³
Assert.InRange(info.Volume, expectedVolume * 0.99, expectedVolume * 1.01);
// Центр масс цилиндра — на оси, по высоте посередине (Z = H/2 = 10 мм).
Assert.InRange(info.CenterX, -0.1, 0.1);
Assert.InRange(info.CenterY, -0.1, 0.1);
Assert.InRange(info.CenterZ, 9.9, 10.1);
// Площадь поверхности: 2·π·R² (торцы) + 2·π·R·H (бок) = 2π·100 + 2π·10·20 ≈ 1884.96 мм².
var expectedArea = 2 * Math.PI * 100 + 2 * Math.PI * 10 * 20;
Assert.InRange(info.Area, expectedArea * 0.98, expectedArea * 1.02);
// Масса стального цилиндра (плотность ≈ 7.856 г/см³): V[см³]·ρ ≈ 6.283·7.856 ≈ 49.36 г = 0.0494 кг.
Assert.InRange(info.Mass, 0.045, 0.055);
}
finally
{
await _docs.CloseAsync(save: false);
}
}
}