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
+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;