28d2ae3868
Новый слой «модель как данные» (приоритетный над 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>
232 lines
11 KiB
C#
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);
|
|
}
|