feat(sketch): эскиз на выбранной грани + МЦХ через API5

sketch_create_on_face: OpenSketchOnFaceAsync выбирает плоскую грань по точке
(ksPart.EntityCollection(o3d_face).SelectByPoint) и создаёт на ней эскиз —
ключевой шаг итеративного цикла «построил → выбрал грань → новый эскиз».
Общее ядро создания эскиза вынесено в CreateSketchOn.

QueryService переписан на API5 ksPart.CalcMassInertiaProperties(ST_MIX_MM|ST_MIX_KG):
- API7 IMassInertiaParam7.Calculate() непригоден — флаг Actual «залипает» после
  первого расчёта, повторный запрос отдавал устаревший объём (баг кэша);
- API5-маршрут считает по запросу и согласован с построением через ksPart;
- единицы (мм³/мм²/кг/мм) задаются битовым вектором.

Тест Sketch_on_face_adds_boss_on_top: запрос → штифт R5×H10 на верхней грани
цилиндра → повторный запрос видит прирост ≈785 мм³ (проверяет и грань, и кэш МЦХ).
Презентация: 20 инструментов, 15 тестов; OPEN_QUESTIONS обновлён.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@
This commit is contained in:
2026-05-26 09:52:56 +03:00
parent b73748f84b
commit 01f5f37c67
6 changed files with 137 additions and 54 deletions
+27 -25
View File
@@ -1,8 +1,8 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using KompasAPI7;
using Kompas6API5;
using Kompas6Constants3D;
namespace Kompas.Mcp.Core.Query;
@@ -10,6 +10,10 @@ namespace Kompas.Mcp.Core.Query;
[SupportedOSPlatform("windows")]
public sealed class QueryService
{
// bitVector для CalcMassInertiaProperties: ST_MIX_MM (0x1, длина — мм) | ST_MIX_KG (0x10, масса — кг).
// Иначе КОМПАС вернёт значения в единицах по умолчанию (см/г).
private const int MixMmKg = 0x1 | 0x10;
private readonly KompasSession _session;
private readonly KompasDispatcher _dispatcher;
@@ -20,41 +24,39 @@ public sealed class QueryService
}
/// <summary>Рассчитать и вернуть МЦХ активной детали (объём, масса, площадь, центр масс).</summary>
/// <remarks>
/// Используется API5 <c>ksPart.CalcMassInertiaProperties</c> — он считает МЦХ «по запросу»
/// и согласован с построением детали через API5. Путь API7 <c>IMassInertiaParam7.Calculate()</c>
/// здесь непригоден: его флаг <c>Actual</c> «залипает» после первого расчёта и не сбрасывается
/// при изменении геометрии через API5 (повторный запрос отдавал бы устаревшее значение).
/// </remarks>
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("Не удалось получить вершинный компонент.");
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).");
ksMassInertiaParam? mass = null;
try
{
var mass = (IMassInertiaParam7)part;
// Детерминированные единицы: длина — мм, масса — кг. Иначе КОМПАС вернёт
// значения в единицах отображения МЦХ (по умолчанию см/г), что зависит от настроек.
mass.LengthUnits = ksLengthUnitsEnum.ksLUnMM;
mass.MassUnits = ksMassUnitsEnum.ksMUnKG;
// Calculate() возвращает FALSE и при успехе (когда расчёт уже актуален),
// поэтому ориентируемся на свойство Actual, а не на возвращаемое значение.
mass.Calculate();
if (!mass.Actual)
throw new InvalidOperationException("МЦХ не рассчитаны (модель пуста или не перестроена?).");
mass = part.CalcMassInertiaProperties(MixMmKg) as ksMassInertiaParam
?? throw new InvalidOperationException("CalcMassInertiaProperties вернул null (модель пуста или не перестроена?).");
return new PartInfo
{
Volume = mass.Volume,
Mass = mass.Mass,
Area = mass.Area,
CenterX = mass.Xc,
CenterY = mass.Yc,
CenterZ = mass.Zc,
Volume = mass.v, // объём, мм³
Mass = mass.m, // масса, кг
Area = mass.F, // площадь, мм²
CenterX = mass.xc, // центр масс в глобальной СК, мм
CenterY = mass.yc,
CenterZ = mass.zc,
};
}
finally
{
// part и его QI-приведение mass — один RCW; release один раз.
if (mass is not null && Marshal.IsComObject(mass)) Marshal.ReleaseComObject(mass);
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
}
}, ct);