@
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:
@@ -69,6 +69,16 @@ stateful-сессии? Пока полагаемся на последовате
|
|||||||
передавать через `ImageContentBlock.FromBytes(bytes, mimeType)` — он сам кодирует. Прямое
|
передавать через `ImageContentBlock.FromBytes(bytes, mimeType)` — он сам кодирует. Прямое
|
||||||
присваивание `Data = rawBytes` портит изображение на проводе. Исправлено в VisionTools.
|
присваивание `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
|
### ✅ Снимок 3D: рендер через файл, а не resultArrayBytes
|
||||||
`ksDocument3D.SaveAsToRasterFormat(fileName, param)` при **непустом** `fileName` возвращает TRUE,
|
`ksDocument3D.SaveAsToRasterFormat(fileName, param)` при **непустом** `fileName` возвращает TRUE,
|
||||||
пишет корректный PNG на диск, но `param.resultArrayBytes` остаётся **пустым**. Поэтому
|
пишет корректный PNG на диск, но `param.resultArrayBytes` остаётся **пустым**. Поэтому
|
||||||
|
|||||||
@@ -125,7 +125,7 @@
|
|||||||
<!-- HERO -->
|
<!-- HERO -->
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<div class="wrap">
|
<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>
|
<h1>КОМПАС-3D <span class="g">MCP-сервер</span><br>управление CAD языком LLM</h1>
|
||||||
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
|
<p class="lead">MCP-сервер, который превращает операции КОМПАС-3D — создание документов,
|
||||||
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
|
эскизы, 3D-операции, параметры — в инструменты для языковой модели. Под капотом —
|
||||||
@@ -246,8 +246,8 @@
|
|||||||
впереди.</p>
|
впереди.</p>
|
||||||
|
|
||||||
<div class="prog">
|
<div class="prog">
|
||||||
<div class="progrow"><span>Общий прогресс</span><span>v1 работает · 12 тестов зелёные · доки и ревью</span></div>
|
<div class="progrow"><span>Общий прогресс</span><span>v1 работает · 14 тестов зелёные · доки и ревью</span></div>
|
||||||
<div class="bar"><i style="width:72%"></i></div>
|
<div class="bar"><i style="width:74%"></i></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="todo-grid" style="margin-top:26px">
|
<div class="todo-grid" style="margin-top:26px">
|
||||||
@@ -282,14 +282,15 @@
|
|||||||
<!-- TOOLS -->
|
<!-- TOOLS -->
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h3>Инструменты v1 <span class="tag done">ГОТОВО</span></h3>
|
<h3>Инструменты v1 <span class="tag done">ГОТОВО</span></h3>
|
||||||
<p class="meta">18 инструментов, отдаются по MCP-протоколу</p>
|
<p class="meta">19 инструментов, отдаются по MCP-протоколу</p>
|
||||||
<ul class="list">
|
<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>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>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>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>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 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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using Kompas.Mcp.Core;
|
using Kompas.Mcp.Core;
|
||||||
using Kompas.Mcp.Core.Documents;
|
using Kompas.Mcp.Core.Documents;
|
||||||
using Kompas.Mcp.Core.Modeling;
|
using Kompas.Mcp.Core.Modeling;
|
||||||
|
using Kompas.Mcp.Core.Query;
|
||||||
using Kompas.Mcp.Core.Threading;
|
using Kompas.Mcp.Core.Threading;
|
||||||
using Kompas.Mcp.Core.Vision;
|
using Kompas.Mcp.Core.Vision;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -19,6 +20,7 @@ builder.Services.AddSingleton<KompasSession>();
|
|||||||
builder.Services.AddSingleton<DocumentService>();
|
builder.Services.AddSingleton<DocumentService>();
|
||||||
builder.Services.AddSingleton<PartModeler>();
|
builder.Services.AddSingleton<PartModeler>();
|
||||||
builder.Services.AddSingleton<SnapshotService>();
|
builder.Services.AddSingleton<SnapshotService>();
|
||||||
|
builder.Services.AddSingleton<QueryService>();
|
||||||
|
|
||||||
// MCP-сервер поверх stdio; инструменты находятся по атрибутам в этой сборке.
|
// MCP-сервер поверх stdio; инструменты находятся по атрибутам в этой сборке.
|
||||||
builder.Services
|
builder.Services
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user