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>
This commit is contained in:
2026-05-26 18:44:38 +03:00
parent 1644ff82e0
commit 28d2ae3868
46 changed files with 3793 additions and 357 deletions
+14
View File
@@ -0,0 +1,14 @@
namespace Kompas.Mcp.Core.Query;
/// <summary>Сводка по телу детали.</summary>
public sealed record BodyInfo
{
/// <summary>Индекс тела в коллекции тел детали.</summary>
public required int Index { get; init; }
/// <summary>true — твёрдое (замкнутое) тело.</summary>
public required bool IsSolid { get; init; }
/// <summary>Число граней тела.</summary>
public required int FaceCount { get; init; }
}
@@ -0,0 +1,25 @@
namespace Kompas.Mcp.Core.Query;
/// <summary>Сводка по компоненту сборки (деталь или подсборка верхнего уровня).</summary>
public sealed record ComponentInfo
{
/// <summary>Индекс в коллекции компонентов верхнего уровня.</summary>
public required int Index { get; init; }
/// <summary>Имя компонента в дереве.</summary>
public required string Name { get; init; }
/// <summary>Обозначение (marking), если задано.</summary>
public required string Marking { get; init; }
/// <summary>Путь к файлу-источнику компонента, если есть.</summary>
public required string FileName { get; init; }
/// <summary>true — деталь, false — подсборка.</summary>
public required bool IsDetail { get; init; }
/// <summary>Габаритные размеры компонента (мм). 0, если габарит недоступен.</summary>
public required double SizeX { get; init; }
public required double SizeY { get; init; }
public required double SizeZ { get; init; }
}
+25
View File
@@ -0,0 +1,25 @@
namespace Kompas.Mcp.Core.Query;
/// <summary>Детальные сведения о ребре (drill-down по индексу из list_edges).</summary>
public sealed record EdgeDetail
{
public required int Index { get; init; }
/// <summary>Тип кривой: line, circle, arc, ellipse, ellipse_arc, nurbs, other.</summary>
public required string Type { get; init; }
/// <summary>Длина ребра, мм.</summary>
public required double Length { get; init; }
/// <summary>Тип одной смежной грани (по «плюс»-ориентации), либо null.</summary>
public string? AdjacentFace1 { get; init; }
/// <summary>Тип другой смежной грани (по «минус»-ориентации), либо null.</summary>
public string? AdjacentFace2 { get; init; }
/// <summary>Координаты начальной вершины (мм), либо null.</summary>
public (double X, double Y, double Z)? Start { get; init; }
/// <summary>Координаты конечной вершины (мм), либо null.</summary>
public (double X, double Y, double Z)? End { get; init; }
}
+22
View File
@@ -0,0 +1,22 @@
namespace Kompas.Mcp.Core.Query;
/// <summary>Детальные сведения о грани (drill-down по индексу из list_faces).</summary>
public sealed record FaceDetail
{
public required int Index { get; init; }
/// <summary>Тип поверхности: plane, cylinder, cone, sphere, torus, nurbs, other.</summary>
public required string Type { get; init; }
/// <summary>Площадь грани, мм².</summary>
public required double Area { get; init; }
/// <summary>Единичная нормаль в центре грани (мировые оси), либо null если недоступна.</summary>
public (double X, double Y, double Z)? Normal { get; init; }
/// <summary>Радиус цилиндрической грани, мм (null для нецилиндрических).</summary>
public double? Radius { get; init; }
/// <summary>Число рёбер, ограничивающих грань.</summary>
public required int EdgeCount { get; init; }
}
+20
View File
@@ -0,0 +1,20 @@
namespace Kompas.Mcp.Core.Query;
/// <summary>Узел дерева построения детали (операция или системный объект).</summary>
public sealed record FeatureInfo
{
/// <summary>Порядковый индекс в дереве построения.</summary>
public required int Index { get; init; }
/// <summary>Имя узла в дереве (локализовано КОМПАС: «Элемент выдавливания:1», «Скругление:1»…).</summary>
public required string Name { get; init; }
/// <summary>
/// Семантический вид операции, определённый по типу её определения:
/// origin, sketch, extrusion, cut_extrusion, revolve, cut_revolve, fillet, chamfer, body, other.
/// </summary>
public required string Kind { get; init; }
/// <summary>Ключевые параметры операции для показа («глубина 15.00 мм», «R3.00 мм»), либо null.</summary>
public string? Detail { get; init; }
}
+119
View File
@@ -0,0 +1,119 @@
using System.Globalization;
using System.Text;
namespace Kompas.Mcp.Core.Query;
/// <summary>
/// Чистый (без COM) рендер структурного «паспорта» модели и вспомогательные форматтеры.
/// Вынесено отдельно, чтобы покрыть логику представления юнит-тестами.
/// </summary>
public static class InspectionText
{
// Габарит пустой детали КОМПАС отдаёт «пустышкой» ~2·10³⁰⁸ (double близко к максимуму).
private const double EmptyGabaritThreshold = 1e12;
/// <summary>true — габарит соответствует пустой модели (sentinel-«пустышка» или нулевой размер).</summary>
public static bool IsEmptyGabarit(double sizeX, double sizeY, double sizeZ)
{
if (double.IsNaN(sizeX) || double.IsNaN(sizeY) || double.IsNaN(sizeZ)) return true;
if (Math.Abs(sizeX) > EmptyGabaritThreshold ||
Math.Abs(sizeY) > EmptyGabaritThreshold ||
Math.Abs(sizeZ) > EmptyGabaritThreshold) return true;
return sizeX == 0 && sizeY == 0 && sizeZ == 0;
}
// Формообразующие операции, наличие которых означает параметрическую историю.
private static readonly string[] OpKinds =
{ "sketch", "extrusion", "cut_extrusion", "revolve", "cut_revolve", "fillet", "chamfer" };
/// <summary>
/// Модель — «голый» импорт без истории построения (типично для STEP): в дереве только начало
/// координат и тело(а), нет ни формообразующих операций, ни иных узлов-правок.
/// </summary>
public static bool IsImported(IReadOnlyList<FeatureInfo> features, int bodyCount)
=> bodyCount > 0
&& !features.Any(f => OpKinds.Contains(f.Kind))
&& features.Count <= bodyCount + 1; // origin + тело(а), ничего сверх
/// <summary>Сгруппировать типы и вернуть «3 plane, 2 cylinder» (по убыванию числа, тай-брейк по имени).</summary>
public static string GroupTypes(IReadOnlyList<string> types)
{
if (types.Count == 0) return "—";
return string.Join(", ", types
.GroupBy(t => t)
.OrderByDescending(g => g.Count())
.ThenBy(g => g.Key, StringComparer.Ordinal)
.Select(g => $"{g.Count()} {g.Key}"));
}
/// <summary>Отрендерить «паспорт» модели в читаемый текст.</summary>
public static string Render(ModelDescription d)
{
var sb = new StringBuilder();
sb.AppendLine($"Документ: «{d.Name}» (деталь)");
if (d.IsEmpty)
{
sb.Append("Модель пуста: нет тел (габарит не определён).");
return sb.ToString();
}
if (d.Box is { } box)
sb.AppendLine(Inv($"Габарит (Д×Ш×В): {box.SizeX:F2} × {box.SizeY:F2} × {box.SizeZ:F2} мм"));
if (d.Mass is { } m)
sb.AppendLine(Inv($"МЦХ: объём {m.Volume:F1} мм³, масса {m.Mass:F4} кг, площадь {m.Area:F1} мм², центр масс ({m.CenterX:F2}, {m.CenterY:F2}, {m.CenterZ:F2})"));
sb.AppendLine($"Тела: {d.Bodies.Count}" + FormatBodies(d.Bodies));
sb.AppendLine($"Топология: граней {d.FaceTypes.Count} ({GroupTypes(d.FaceTypes)}); " +
$"рёбер {d.EdgeTypes.Count} ({GroupTypes(d.EdgeTypes)})");
AppendTree(sb, d);
AppendVariables(sb, d.Variables);
return sb.ToString().TrimEnd();
}
private static string FormatBodies(IReadOnlyList<BodyInfo> bodies)
{
if (bodies.Count == 0) return "";
var parts = bodies.Select(b => $"тело #{b.Index}{(b.IsSolid ? " (твёрдое)" : "")}, граней {b.FaceCount}");
return " — " + string.Join("; ", parts);
}
private static void AppendTree(StringBuilder sb, ModelDescription d)
{
if (d.Imported)
{
var names = string.Join(", ", d.Features.Select(f => $"«{f.Name}»"));
sb.AppendLine("Дерево построения: импортированная модель без истории построения " +
"(доступна только топология)." + (names.Length > 0 ? " Узлы: " + names + "." : ""));
return;
}
if (d.Features.Count == 0)
{
sb.AppendLine("Дерево построения: нет операций.");
return;
}
sb.AppendLine($"Дерево построения ({d.Features.Count} операц.):");
foreach (var f in d.Features)
sb.AppendLine($" [{f.Index}] «{f.Name}»" + (string.IsNullOrEmpty(f.Detail) ? "" : $" — {f.Detail}"));
}
private static void AppendVariables(StringBuilder sb, IReadOnlyList<VariableInfo> vars)
{
if (vars.Count == 0) { sb.AppendLine("Переменные: нет"); return; }
sb.AppendLine($"Переменные ({vars.Count}):");
foreach (var v in vars)
{
var flags = (v.External ? " [внешняя]" : "") + (v.Information ? " [инфо]" : "");
sb.AppendLine(Inv($" {v.Name} = {v.Expression} (= {v.Value:G}){flags}"));
}
}
private static string Inv(FormattableString fs) => FormattableString.Invariant(fs);
}
@@ -0,0 +1,17 @@
namespace Kompas.Mcp.Core.Query;
/// <summary>Результат измерения между двумя объектами модели (грань/ребро/вершина).</summary>
public sealed record MeasureResult
{
/// <summary>Расстояние, мм (для совпадающих/пересекающихся объектов — 0).</summary>
public required double Distance { get; init; }
/// <summary>Минимальное расстояние, мм.</summary>
public required double MinDistance { get; init; }
/// <summary>Угол между объектами, градусы (валиден только когда <see cref="AngleValid"/>).</summary>
public required double Angle { get; init; }
/// <summary>true — угол применим к данной паре объектов.</summary>
public required bool AngleValid { get; init; }
}
@@ -0,0 +1,41 @@
namespace Kompas.Mcp.Core.Query;
/// <summary>
/// Структурный «паспорт» активной детали: всё, что нужно агенту, чтобы «увидеть» модель
/// без растрового снимка. Заполняется <c>ModelInspectionService</c>, рендерится <c>InspectionText</c>.
/// </summary>
public sealed record ModelDescription
{
/// <summary>Имя документа/детали.</summary>
public required string Name { get; init; }
/// <summary>Модель пуста (нет тел; габарит-«пустышка»).</summary>
public required bool IsEmpty { get; init; }
/// <summary>Габарит детали (null для пустой модели).</summary>
public BoundingBox? Box { get; init; }
/// <summary>Массово-центровочные характеристики (null для пустой модели).</summary>
public PartInfo? Mass { get; init; }
/// <summary>Тела детали.</summary>
public required IReadOnlyList<BodyInfo> Bodies { get; init; }
/// <summary>Типы всех граней (для сводки по топологии).</summary>
public required IReadOnlyList<string> FaceTypes { get; init; }
/// <summary>Типы всех рёбер (для сводки по топологии).</summary>
public required IReadOnlyList<string> EdgeTypes { get; init; }
/// <summary>Узлы дерева построения по порядку.</summary>
public required IReadOnlyList<FeatureInfo> Features { get; init; }
/// <summary>
/// Модель импортирована (дерево содержит только начало координат и тело(а), без
/// параметрической истории) — типично для импорта STEP.
/// </summary>
public required bool Imported { get; init; }
/// <summary>Переменные модели.</summary>
public required IReadOnlyList<VariableInfo> Variables { get; init; }
}
@@ -0,0 +1,425 @@
using System.Globalization;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using Kompas6Constants3D;
namespace Kompas.Mcp.Core.Query;
/// <summary>
/// Структурное «зрение» агента: чтение модели как данных, а не картинки — дерево построения,
/// тела, топология, переменные, drill-down по граням/рёбрам и измерения. Всё через API5
/// <c>ksPart</c> на STA-потоке <see cref="KompasDispatcher"/>.
/// </summary>
[SupportedOSPlatform("windows")]
public sealed class ModelInspectionService
{
private const uint MixMm = 0x1; // длины/площади — мм
private const int MixMmKg = 0x1 | 0x10; // МЦХ — мм + кг
private readonly KompasSession _session;
private readonly KompasDispatcher _dispatcher;
public ModelInspectionService(KompasSession session, KompasDispatcher dispatcher)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
}
/// <summary>Собрать структурный «паспорт» активной детали одним проходом.</summary>
public Task<ModelDescription> DescribeModelAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
try
{
var name = ActiveDocName();
bool hasGab = part.GetGabarit(false, false,
out double x1, out double y1, out double z1, out double x2, out double y2, out double z2);
var box = hasGab ? Box(x1, y1, z1, x2, y2, z2) : null;
bool empty = box is null || InspectionText.IsEmptyGabarit(box.SizeX, box.SizeY, box.SizeZ);
if (empty)
return new ModelDescription
{
Name = name, IsEmpty = true,
Bodies = Array.Empty<BodyInfo>(), FaceTypes = Array.Empty<string>(),
EdgeTypes = Array.Empty<string>(), Features = Array.Empty<FeatureInfo>(),
Imported = false, Variables = Array.Empty<VariableInfo>(),
};
var bodies = ReadBodies(part);
var (faceTypes, edgeTypes) = ReadTopology(part);
var features = ReadFeatures(part);
var variables = ReadVariables(part);
return new ModelDescription
{
Name = name,
IsEmpty = false,
Box = box,
Mass = ReadMass(part),
Bodies = bodies,
FaceTypes = faceTypes,
EdgeTypes = edgeTypes,
Features = features,
Imported = InspectionText.IsImported(features, bodies.Count),
Variables = variables,
};
}
finally { Release(part); }
}, ct);
/// <summary>Перечислить узлы дерева построения по порядку.</summary>
public Task<IReadOnlyList<FeatureInfo>> ListFeaturesAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
try { return ReadFeatures(part); }
finally { Release(part); }
}, ct);
/// <summary>Перечислить тела детали.</summary>
public Task<IReadOnlyList<BodyInfo>> ListBodiesAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
try { return ReadBodies(part); }
finally { Release(part); }
}, ct);
/// <summary>Перечислить переменные модели.</summary>
public Task<IReadOnlyList<VariableInfo>> ListVariablesAsync(CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
try { return ReadVariables(part); }
finally { Release(part); }
}, ct);
/// <summary>Детали грани по индексу из list_faces: тип, площадь, нормаль, радиус, число рёбер.</summary>
public Task<FaceDetail> DescribeFaceAsync(int index, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
try
{
var faces = Faces(part);
var def = FaceAt(faces, index);
double area = 0; try { area = def.GetArea(MixMm); } catch { }
int edgeCount = 0; try { if (def.EdgeCollection() is ksEdgeCollection ec) edgeCount = ec.GetCount(); } catch { }
double? radius = null;
try { if (def.IsCylinder() && def.GetCylinderParam(out _, out double r)) radius = r; } catch { }
return new FaceDetail
{
Index = index,
Type = FaceType(def),
Area = area,
Normal = FaceNormal(def),
Radius = radius,
EdgeCount = edgeCount,
};
}
finally { Release(part); }
}, ct);
/// <summary>Детали ребра по индексу из list_edges: тип, длина, смежные грани, концевые вершины.</summary>
public Task<EdgeDetail> DescribeEdgeAsync(int index, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
try
{
var edges = Edges(part);
var def = EdgeAt(edges, index);
double len = 0; try { len = def.GetLength(MixMm); } catch { }
string? f1 = null, f2 = null;
try { if (def.GetAdjacentFace(true) is ksFaceDefinition a) f1 = FaceType(a); } catch { }
try { if (def.GetAdjacentFace(false) is ksFaceDefinition b) f2 = FaceType(b); } catch { }
return new EdgeDetail
{
Index = index,
Type = EdgeType(def),
Length = len,
AdjacentFace1 = f1,
AdjacentFace2 = f2,
Start = Vertex(def, true),
End = Vertex(def, false),
};
}
finally { Release(part); }
}, ct);
/// <summary>
/// Измерить расстояние и угол между двумя объектами модели. <paramref name="kind1"/>/<paramref name="kind2"/>
/// — face|edge|vertex; индексы — из list_faces/list_edges.
/// </summary>
public Task<MeasureResult> MeasureAsync(string kind1, int index1, string kind2, int index2, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() =>
{
var part = GetTopPart();
try
{
var o1 = ResolveObject(part, kind1, index1);
var o2 = ResolveObject(part, kind2, index2);
var meas = part.GetMeasurer() as ksMeasurer
?? throw new InvalidOperationException("GetMeasurer вернул null.");
meas.SetObject1(o1);
meas.SetObject2(o2);
meas.unit = MixMm;
if (!meas.Calc())
throw new InvalidOperationException("Измерение не выполнено (Calc вернул FALSE).");
bool angleValid = false; try { angleValid = meas.IsAngleValid(); } catch { }
return new MeasureResult
{
Distance = meas.distance,
MinDistance = meas.MinDistance,
Angle = angleValid ? meas.angle : 0,
AngleValid = angleValid,
};
}
finally { Release(part); }
}, ct);
// ── чтение данных (на STA-потоке) ─────────────────────────────────────
private static IReadOnlyList<FeatureInfo> ReadFeatures(ksPart part)
{
// Дерево детали — через ksPart.GetFeature() (приведение part as ksFeature не срабатывает).
var root = part.GetFeature() as ksFeature ?? part as ksFeature;
if (root?.SubFeatureCollection(true, false) is not ksFeatureCollection tree) return Array.Empty<FeatureInfo>();
int n = tree.GetCount();
var list = new List<FeatureInfo>(n);
for (int i = 0; i < n; i++)
{
if (tree.GetByIndex(i) is not ksFeature f) continue;
string name = ""; try { name = f.name ?? ""; } catch { }
var (kind, detail) = ClassifyFeature(f, name);
list.Add(new FeatureInfo { Index = i, Name = name, Kind = kind, Detail = detail });
}
return list;
}
// ksFeature.type отдаёт лишь coarse o3d_entity → точный вид и параметры берём из определения.
private static (string kind, string? detail) ClassifyFeature(ksFeature f, string name)
{
try
{
if (f.GetObject() is ksEntity ent)
switch (ent.GetDefinition())
{
case ksBossExtrusionDefinition be:
return ("extrusion", be.GetSideParam(true, out _, out double d1, out _, out _) ? Inv($"глубина {d1:F2} мм") : null);
case ksCutExtrusionDefinition ce:
return ("cut_extrusion", ce.GetSideParam(true, out _, out double d2, out _, out _) ? Inv($"вырез, глубина {d2:F2} мм") : "вырез");
case ksFilletDefinition fd:
return ("fillet", Inv($"R{fd.radius:F2} мм"));
case ksChamferDefinition cd:
return ("chamfer", cd.GetChamferParam(out _, out double s1, out double s2) ? Inv($"{s1:F2}×{s2:F2} мм") : null);
case ksBossRotatedDefinition:
return ("revolve", null);
case ksCutRotatedDefinition:
return ("cut_revolve", "вырез вращением");
case ksSketchDefinition:
return ("sketch", null);
}
}
catch { /* параметры/тип best-effort */ }
// Не распознанная операция: начало координат, импортированное тело либо иной узел-правка.
return name.StartsWith("Начало", StringComparison.OrdinalIgnoreCase) ? ("origin", null) : ("other", null);
}
private static IReadOnlyList<BodyInfo> ReadBodies(ksPart part)
{
if (part.BodyCollection() is not ksBodyCollection bc) return Array.Empty<BodyInfo>();
int n = bc.GetCount();
var list = new List<BodyInfo>(n);
for (int i = 0; i < n; i++)
{
if (bc.GetByIndex(i) is not ksBody b) continue;
int faces = 0; try { if (b.FaceCollection() is ksFaceCollection fc) faces = fc.GetCount(); } catch { }
bool solid = false; try { solid = b.IsSolid(); } catch { }
list.Add(new BodyInfo { Index = i, IsSolid = solid, FaceCount = faces });
}
return list;
}
private static IReadOnlyList<VariableInfo> ReadVariables(ksPart part)
{
if (part.VariableCollection() is not ksVariableCollection vc) return Array.Empty<VariableInfo>();
int n = vc.GetCount();
var list = new List<VariableInfo>(n);
for (int i = 0; i < n; i++)
{
if (vc.GetByIndex(i) is not ksVariable v) continue;
list.Add(new VariableInfo
{
Name = SafeStr(() => v.name),
Expression = SafeStr(() => v.Expression),
Value = SafeNum(() => v.value),
External = SafeBool(() => v.external),
Information = SafeBool(() => v.Information),
});
}
return list;
}
private static (List<string> faceTypes, List<string> edgeTypes) ReadTopology(ksPart part)
{
var faceTypes = new List<string>();
var edgeTypes = new List<string>();
if (Faces(part) is { } faces)
for (int i = 0; i < faces.GetCount(); i++)
if (faces.GetByIndex(i) is ksEntity e && e.GetDefinition() is ksFaceDefinition d) faceTypes.Add(FaceType(d));
if (Edges(part) is { } edges)
for (int i = 0; i < edges.GetCount(); i++)
if (edges.GetByIndex(i) is ksEntity e && e.GetDefinition() is ksEdgeDefinition d) edgeTypes.Add(EdgeType(d));
return (faceTypes, edgeTypes);
}
private static PartInfo? ReadMass(ksPart part)
{
try
{
if (part.CalcMassInertiaProperties(MixMmKg) is not ksMassInertiaParam m) return null;
return new PartInfo
{
Volume = m.v, Mass = m.m, Area = m.F,
CenterX = m.xc, CenterY = m.yc, CenterZ = m.zc,
};
}
catch { return null; }
}
private static (double X, double Y, double Z)? FaceNormal(ksFaceDefinition def)
{
try
{
if (def.GetSurface() is not ksSurface surf) return null;
double u = (surf.GetParamUMin() + surf.GetParamUMax()) / 2;
double v = (surf.GetParamVMin() + surf.GetParamVMax()) / 2;
if (!surf.GetNormal(u, v, out double nx, out double ny, out double nz)) return null;
if (!def.normalOrientation) { nx = -nx; ny = -ny; nz = -nz; }
return (nx, ny, nz);
}
catch { return null; }
}
private static (double X, double Y, double Z)? Vertex(ksEdgeDefinition def, bool start)
{
try
{
if (def.GetVertex(start) is ksVertexDefinition v && v.GetPoint(out double x, out double y, out double z))
return (x, y, z);
}
catch { }
return null;
}
private object ResolveObject(ksPart part, string kind, int index)
{
var type = kind.Trim().ToLowerInvariant() switch
{
"face" => Obj3dType.o3d_face,
"edge" => Obj3dType.o3d_edge,
"vertex" => Obj3dType.o3d_vertex,
_ => throw new ArgumentException($"Неизвестный тип объекта '{kind}'. Допустимо: face, edge, vertex."),
};
var coll = part.EntityCollection((short)type) as ksEntityCollection
?? throw new InvalidOperationException($"Не удалось получить коллекцию {kind}.");
if (index < 0 || index >= coll.GetCount())
throw new ArgumentOutOfRangeException(nameof(index), $"Индекс {kind} вне диапазона [0; {coll.GetCount() - 1}].");
return coll.GetByIndex(index) as ksEntity
?? throw new InvalidOperationException($"Объект {kind}[{index}] не приводится к ksEntity.");
}
// ── низкоуровневые помощники ──────────────────────────────────────────
private ksPart GetTopPart()
{
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
?? throw new InvalidOperationException("Нет активного 3D-документа.");
return doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
}
private string ActiveDocName()
{
var doc = _session.Application.ActiveDocument;
try { return doc?.Name ?? ""; }
catch { return ""; }
finally { Release(doc); }
}
private static ksEntityCollection? Faces(ksPart part) => part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection;
private static ksEntityCollection? Edges(ksPart part) => part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection;
private static ksFaceDefinition FaceAt(ksEntityCollection? faces, int index)
{
if (faces is null) throw new InvalidOperationException("Не удалось получить коллекцию граней.");
if (index < 0 || index >= faces.GetCount())
throw new ArgumentOutOfRangeException(nameof(index), $"Индекс грани вне диапазона [0; {faces.GetCount() - 1}]. Сверьтесь с list_faces.");
return (faces.GetByIndex(index) as ksEntity)?.GetDefinition() as ksFaceDefinition
?? throw new InvalidOperationException($"Грань[{index}] не приводится к ksFaceDefinition.");
}
private static ksEdgeDefinition EdgeAt(ksEntityCollection? edges, int index)
{
if (edges is null) throw new InvalidOperationException("Не удалось получить коллекцию рёбер.");
if (index < 0 || index >= edges.GetCount())
throw new ArgumentOutOfRangeException(nameof(index), $"Индекс ребра вне диапазона [0; {edges.GetCount() - 1}]. Сверьтесь с list_edges.");
return (edges.GetByIndex(index) as ksEntity)?.GetDefinition() as ksEdgeDefinition
?? throw new InvalidOperationException($"Ребро[{index}] не приводится к ksEdgeDefinition.");
}
private static string FaceType(ksFaceDefinition d)
{
try { if (d.IsPlanar()) return "plane"; } catch { }
try { if (d.IsCylinder()) return "cylinder"; } catch { }
try { if (d.IsCone()) return "cone"; } catch { }
try { if (d.IsSphere()) return "sphere"; } catch { }
try { if (d.IsTorus()) return "torus"; } catch { }
try { if (d.IsNurbsSurface()) return "nurbs"; } catch { }
return "other";
}
private static string EdgeType(ksEdgeDefinition d)
{
try { if (d.IsLineSeg()) return "line"; } catch { }
try { if (d.IsCircle()) return "circle"; } catch { }
try { if (d.IsArc()) return "arc"; } catch { }
try { if (d.IsEllipseArc()) return "ellipse_arc"; } catch { }
try { if (d.IsEllipse()) return "ellipse"; } catch { }
try { if (d.IsNurbs()) return "nurbs"; } catch { }
return "other";
}
private static BoundingBox Box(double x1, double y1, double z1, double x2, double y2, double z2) => new()
{
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),
};
private static string SafeStr(Func<string?> g) { try { return g() ?? ""; } catch { return ""; } }
private static double SafeNum(Func<double> g) { try { return g(); } catch { return 0; } }
private static bool SafeBool(Func<bool> g) { try { return g(); } catch { return false; } }
private static string Inv(FormattableString fs) => FormattableString.Invariant(fs);
private static void Release(object? com)
{
if (com is not null && Marshal.IsComObject(com))
{
try { Marshal.ReleaseComObject(com); } catch { }
}
}
}
+47
View File
@@ -61,6 +61,53 @@ public sealed class QueryService
}
}, 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;
+19
View File
@@ -0,0 +1,19 @@
namespace Kompas.Mcp.Core.Query;
/// <summary>Переменная модели (имя, выражение, значение, признаки).</summary>
public sealed record VariableInfo
{
public required string Name { get; init; }
/// <summary>Выражение переменной (может быть числом или формулой со ссылками на другие переменные).</summary>
public required string Expression { get; init; }
/// <summary>Вычисленное числовое значение.</summary>
public required double Value { get; init; }
/// <summary>Внешняя переменная (доступна из вышестоящих документов/исполнений).</summary>
public required bool External { get; init; }
/// <summary>Информационная переменная (вычисляется, не задаёт геометрию).</summary>
public required bool Information { get; init; }
}