Files
kompas3d-mcp/src/Kompas.Mcp.Core/Query/QueryService.cs
T
mikhail 28d2ae3868 feat(inspection): структурное «зрение» модели — осмотр без снимка
Новый слой «модель как данные» (приоритетный над model_snapshot):
- describe_model — структурный «паспорт» детали одним вызовом (габарит,
  МЦХ, тела, сводка топологии, дерево построения с параметрами, переменные)
- list_features / list_bodies / list_variables
- describe_face / describe_edge (drill-down по индексу)
- measure (расстояние/угол между гранями/рёбрами/вершинами)
- model_snapshot понижен до fallback для визуально-пространственных вопросов

Реализация: ModelInspectionService (COM) + чистый InspectionText (рендер,
группировка, детект «импорт без истории») + record-модели; InspectionTools.
Дерево читается через ksPart.GetFeature()->SubFeatureCollection; точный тип
и параметры операций берутся из GetObject()->GetDefinition() (ksFeature.type
отдаёт лишь coarse o3d_entity).

Снимок дерева также включает слои conversion (STEP), editing (move_face/solid)
и validation, развивавшиеся параллельно. Полный прогон: 61 тест зелёный,
43 MCP-инструмента. Документация (README, ARCHITECTURE, OPEN_QUESTIONS,
CLAUDE.md, presentation.html) синхронизирована.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:44:38 +03:00

232 lines
11 KiB
C#

using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using Kompas6Constants3D;
namespace Kompas.Mcp.Core.Query;
/// <summary>Запросы к активной 3D-модели: массово-центровочные характеристики.</summary>
[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;
public QueryService(KompasSession session, KompasDispatcher dispatcher)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
/// <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.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
{
mass = part.CalcMassInertiaProperties(MixMmKg) as ksMassInertiaParam
?? throw new InvalidOperationException("CalcMassInertiaProperties вернул null (модель пуста или не перестроена?).");
return new PartInfo
{
Volume = mass.v, // объём, мм³
Mass = mass.m, // масса, кг
Area = mass.F, // площадь, мм²
CenterX = mass.xc, // центр масс в глобальной СК, мм
CenterY = mass.yc,
CenterZ = mass.zc,
};
}
finally
{
if (mass is not null && Marshal.IsComObject(mass)) Marshal.ReleaseComObject(mass);
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
}
}, ct);
/// <summary>
/// Перечислить компоненты верхнего уровня активной сборки (детали/подсборки): имя,
/// обозначение, файл, признак детали и габарит. Обход через API7 <c>TopPart → Parts</c>.
/// </summary>
public Task<IReadOnlyList<ComponentInfo>> ListComponentsAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync<IReadOnlyList<ComponentInfo>>(() =>
{
if (_session.Application.ActiveDocument is not KompasAPI7.IKompasDocument3D doc3d)
throw new InvalidOperationException("Активный документ не является 3D-документом.");
if (doc3d.TopPart is not KompasAPI7.IPart7 top)
throw new InvalidOperationException("Не удалось получить TopPart активного документа.");
var parts = top.Parts;
int count = parts.Count;
var list = new List<ComponentInfo>(count);
for (int i = 0; i < count; i++)
{
if (parts.Part[i] is not KompasAPI7.IPart7 p) continue;
double x1 = 0, y1 = 0, z1 = 0, x2 = 0, y2 = 0, z2 = 0;
bool gab = false;
try { gab = p.GetGabarit(false, false, out x1, out y1, out z1, out x2, out y2, out z2); }
catch { /* габарит может быть недоступен у отдельных компонентов */ }
list.Add(new ComponentInfo
{
Index = i,
Name = SafeGet(() => p.Name),
Marking = SafeGet(() => p.Marking),
FileName = SafeGet(() => p.FileName),
IsDetail = SafeBool(() => p.Detail),
SizeX = gab ? Math.Abs(x2 - x1) : 0,
SizeY = gab ? Math.Abs(y2 - y1) : 0,
SizeZ = gab ? Math.Abs(z2 - z1) : 0,
});
}
return list;
}, ct);
private static string SafeGet(Func<string?> get)
{
try { return get() ?? string.Empty; } catch { return string.Empty; }
}
private static bool SafeBool(Func<bool> get)
{
try { return get(); } catch { return false; }
}
// ST_MIX_MM (0x1) — площади граней в мм².
private const uint MixMm = 0x1;
/// <summary>Перечислить грани активной детали: индекс, тип поверхности и площадь (мм²).</summary>
public Task<IReadOnlyList<FaceInfo>> ListFacesAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync<IReadOnlyList<FaceInfo>>(() =>
{
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).");
try
{
var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection
?? throw new InvalidOperationException("Не удалось получить коллекцию граней.");
var count = faces.GetCount();
var list = new List<FaceInfo>(count);
for (int i = 0; i < count; i++)
{
if (faces.GetByIndex(i) is not ksEntity entity) continue;
if (entity.GetDefinition() is not ksFaceDefinition def) continue;
list.Add(new FaceInfo
{
Index = i,
Type = FaceType(def),
Area = def.GetArea(MixMm),
});
}
return list;
}
finally
{
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
}
}, ct);
private static string FaceType(ksFaceDefinition def)
{
if (def.IsPlanar()) return "plane";
if (def.IsCylinder()) return "cylinder";
if (def.IsCone()) return "cone";
if (def.IsSphere()) return "sphere";
if (def.IsTorus()) return "torus";
if (def.IsNurbsSurface()) return "nurbs";
return "other";
}
/// <summary>Перечислить рёбра активной детали: индекс, тип кривой и длину (мм).</summary>
public Task<IReadOnlyList<EdgeInfo>> ListEdgesAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync<IReadOnlyList<EdgeInfo>>(() =>
{
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).");
try
{
var edges = part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection
?? throw new InvalidOperationException("Не удалось получить коллекцию рёбер.");
var count = edges.GetCount();
var list = new List<EdgeInfo>(count);
for (int i = 0; i < count; i++)
{
if (edges.GetByIndex(i) is not ksEntity entity) continue;
if (entity.GetDefinition() is not ksEdgeDefinition def) continue;
list.Add(new EdgeInfo
{
Index = i,
Type = EdgeType(def),
Length = def.GetLength(MixMm),
});
}
return list;
}
finally
{
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
}
}, ct);
private static string EdgeType(ksEdgeDefinition def)
{
if (def.IsLineSeg()) return "line";
if (def.IsCircle()) return "circle";
if (def.IsArc()) return "arc";
if (def.IsEllipseArc()) return "ellipse_arc";
if (def.IsEllipse()) return "ellipse";
if (def.IsNurbs()) return "nurbs";
return "other";
}
/// <summary>Габаритный параллелепипед активной детали (по телам), мм.</summary>
public Task<BoundingBox> GetBoundingBoxAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
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).");
try
{
// full=false → только тела (без вспомогательной геометрии); customizable игнорируется.
if (!part.GetGabarit(false, false,
out double x1, out double y1, out double z1,
out double x2, out double y2, out double z2))
throw new InvalidOperationException("GetGabarit вернул FALSE (модель пуста?).");
return new BoundingBox
{
MinX = Math.Min(x1, x2), MinY = Math.Min(y1, y2), MinZ = Math.Min(z1, z2),
MaxX = Math.Max(x1, x2), MaxY = Math.Max(y1, y2), MaxZ = Math.Max(z1, z2),
};
}
finally
{
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
}
}, ct);
}