refactor(core): унификация COM-хелперов и классификаторов топологии

Вынесены общие ComHelper (Release + Safe-геттеры) и GeomClassifiers
(FaceType/EdgeType), устранены дубли в 5+ сервисах. Освобождение doc3d RCW
в GetTopPart, void-возвраты вместо фиктивных bool, HashSet для OpKinds,
DocumentTypeName в паспорте модели, FileExtension без аллокаций.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 20:15:43 +03:00
parent e1337e4016
commit 579b191d7c
13 changed files with 229 additions and 188 deletions
@@ -1,6 +1,6 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Interop;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using KompasAPI7;
@@ -39,7 +39,7 @@ public sealed class ConversionService
CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => ImportCore(path, docType, createComponentFiles), ct);
private bool ImportCore(string path, KompasDocumentType docType, bool createComponentFiles)
private void ImportCore(string path, KompasDocumentType docType, bool createComponentFiles)
{
if (!File.Exists(path))
throw new FileNotFoundException($"STEP-файл не найден: {path}", path);
@@ -63,19 +63,22 @@ public sealed class ConversionService
prm.Format = (ksKOMPASConverterEnum)StepFormats.ImportConverterCode;
if (createComponentFiles)
{
// NeedCreateComponentsFiles может отсутствовать в Home-редакции КОМПАС —
// ловим COM-исключения; не критично, если свойство недоступно.
try { prm.NeedCreateComponentsFiles = true; }
catch { /* свойство может быть недоступно — не критично */ }
catch (System.Runtime.InteropServices.COMException) { }
catch (System.Runtime.InteropServices.InvalidComObjectException) { }
catch (NotImplementedException) { }
}
if (!doc3d1.ConvertFromAdditionFormat(path, (AdditionConvertParameters)prm))
throw new InvalidOperationException("ConvertFromAdditionFormat вернул FALSE (импорт STEP не удался).");
return true;
}
finally
{
ReleaseCom(prm);
ReleaseCom(doc);
ReleaseCom(conv);
ComHelper.Release(prm);
ComHelper.Release(doc);
ComHelper.Release(conv);
}
}
@@ -83,7 +86,7 @@ public sealed class ConversionService
public Task ExportStepAsync(string path, StepFormat format = StepFormat.Auto, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => ExportCore(path, format), ct);
private bool ExportCore(string path, StepFormat format)
private void ExportCore(string path, StepFormat format)
{
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Путь экспорта не задан.", nameof(path));
@@ -109,13 +112,12 @@ public sealed class ConversionService
if (!doc3d1.ConvertToAdditionFormat(path, (AdditionConvertParameters)prm))
throw new InvalidOperationException("ConvertToAdditionFormat вернул FALSE (экспорт STEP не удался).");
return true;
}
finally
{
ReleaseCom(prm);
ReleaseCom(active);
ReleaseCom(conv);
ComHelper.Release(prm);
ComHelper.Release(active);
ComHelper.Release(conv);
}
}
@@ -131,13 +133,4 @@ public sealed class ConversionService
throw new InvalidOperationException(
$"get_Converter({formatCode}) вернул null — STEP-конвертер недоступен в этой редакции КОМПАС.");
}
private static void ReleaseCom(object? comObject)
{
if (comObject is not null && Marshal.IsComObject(comObject))
{
try { Marshal.ReleaseComObject(comObject); }
catch { /* освобождение не критично */ }
}
}
}
@@ -1,5 +1,5 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Interop;
using Kompas.Mcp.Core.Threading;
using Kompas6Constants;
using KompasAPI7;
@@ -26,7 +26,7 @@ public sealed class DocumentService
var docs = _session.Application.Documents;
var doc = (IKompasDocument)docs.Add(DocumentTypes.ToEnum(type), visible);
try { return ToInfo(doc); }
finally { ReleaseCom(doc); ReleaseCom(docs); }
finally { ComHelper.Release(doc); ComHelper.Release(docs); }
}, ct);
/// <summary>Открыть документ с диска.</summary>
@@ -38,7 +38,7 @@ public sealed class DocumentService
if (doc is null)
throw new InvalidOperationException($"Не удалось открыть документ: {path}");
try { return ToInfo(doc); }
finally { ReleaseCom(doc); ReleaseCom(docs); }
finally { ComHelper.Release(doc); ComHelper.Release(docs); }
}, ct);
/// <summary>Сводка по активному документу или <c>null</c>, если документов нет.</summary>
@@ -48,7 +48,7 @@ public sealed class DocumentService
var doc = _session.Application.ActiveDocument;
if (doc is null) return null;
try { return ToInfo(doc); }
finally { ReleaseCom(doc); }
finally { ComHelper.Release(doc); }
}, ct);
/// <summary>Сохранить активный документ (для уже сохранённого — без диалога).</summary>
@@ -72,7 +72,7 @@ public sealed class DocumentService
var doc = _session.Application.ActiveDocument
?? throw new InvalidOperationException("Нет активного документа.");
try { return func(doc); }
finally { ReleaseCom(doc); }
finally { ComHelper.Release(doc); }
}
private static DocumentInfo ToInfo(IKompasDocument doc) => new()
@@ -81,13 +81,4 @@ public sealed class DocumentService
PathName = doc.PathName ?? string.Empty,
Type = DocumentTypes.FromEnum(doc.DocumentType),
};
private static void ReleaseCom(object? comObject)
{
if (comObject is not null && Marshal.IsComObject(comObject))
{
try { Marshal.ReleaseComObject(comObject); }
catch { /* освобождение не критично */ }
}
}
}
@@ -28,7 +28,7 @@ public sealed class FaceEditService
public Task MoveFaceAsync(double x, double y, double z, double distance, CancellationToken ct = default)
=> _dispatcher.InvokeAsync(() => MoveFaceCore(x, y, z, distance), ct);
private bool MoveFaceCore(double x, double y, double z, double distance)
private void MoveFaceCore(double x, double y, double z, double distance)
{
if (distance == 0) throw new ArgumentException("Смещение не может быть нулевым.", nameof(distance));
@@ -61,7 +61,6 @@ public sealed class FaceEditService
// Перестраиваем, чтобы состояние ошибки операции стало актуальным (проверяется через validate_part).
doc3d.RebuildDocument();
return true;
}
private static IFace? FindFaceAtPoint(IPart7 part, double x, double y, double z)
+30
View File
@@ -0,0 +1,30 @@
using System.Runtime.InteropServices;
namespace Kompas.Mcp.Core.Interop;
/// <summary>
/// Вспомогательные методы для работы с COM RCW: безопасное освобождение
/// и safe-геттеры для свойств, которые могут бросить исключение.
/// <para>
/// Метод <see cref="Release"/> идентично реализован в каждом сервисе — вынесен сюда.
/// Safe-геттеры заменяют разрозненные <c>SafeStr</c>/<c>SafeGet</c>/<c>SafeBool</c>/<c>SafeNum</c>.
/// </para>
/// </summary>
internal static class ComHelper
{
/// <summary>Освободить COM RCW: null-гард + IsComObject-проверка + try/catch.</summary>
internal static void Release(object? com)
{
if (com is not null && Marshal.IsComObject(com))
try { Marshal.ReleaseComObject(com); } catch { }
}
/// <summary>Безопасно получить строку: возвращает <c>""</c> при любом исключении.</summary>
internal static string SafeStr(Func<string?> f) { try { return f() ?? ""; } catch { return ""; } }
/// <summary>Безопасно получить число: возвращает <c>0</c> при любом исключении.</summary>
internal static double SafeNum(Func<double> f) { try { return f(); } catch { return 0; } }
/// <summary>Безопасно получить флаг: возвращает <c>false</c> при любом исключении.</summary>
internal static bool SafeBool(Func<bool> f) { try { return f(); } catch { return false; } }
}
+5 -5
View File
@@ -65,7 +65,7 @@ public sealed class KompasSession : IDisposable
private KompasConnectionInfo ConnectCore(bool visible)
{
if (_app is not null)
return LastInfo ??= BuildInfo(LastProgId);
return LastInfo ??= BuildInfo(_lastProgId);
// 1) attach к уже запущенному экземпляру
foreach (var id in ProgIds)
@@ -73,7 +73,7 @@ public sealed class KompasSession : IDisposable
if (NativeMethods.GetActiveObject(id) is KompasObject ko)
{
_kompas = ko;
LastProgId = id;
_lastProgId = id;
_launchedByUs = false;
_log.LogInformation("Присоединились к запущенному КОМПАС ({ProgId})", id);
break;
@@ -92,7 +92,7 @@ public sealed class KompasSession : IDisposable
if (Activator.CreateInstance(type) is KompasObject ko)
{
_kompas = ko;
LastProgId = id;
_lastProgId = id;
_launchedByUs = true;
_log.LogInformation("Запустили новый КОМПАС ({ProgId})", id);
break;
@@ -114,10 +114,10 @@ public sealed class KompasSession : IDisposable
_app.Visible = visible;
return LastInfo = BuildInfo(LastProgId);
return LastInfo = BuildInfo(_lastProgId);
}
private string LastProgId { get; set; } = string.Empty;
private string _lastProgId = string.Empty;
private KompasConnectionInfo BuildInfo(string progId)
{
+2 -9
View File
@@ -1,5 +1,5 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Interop;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using Kompas6Constants3D;
@@ -432,14 +432,7 @@ public sealed class PartModeler : IDisposable
_nextId = 1;
}
private static void ReleaseCom(object? comObject)
{
if (comObject is not null && Marshal.IsComObject(comObject))
{
try { Marshal.ReleaseComObject(comObject); }
catch { /* освобождение не критично */ }
}
}
private static void ReleaseCom(object? comObject) => ComHelper.Release(comObject);
public void Dispose()
{
@@ -0,0 +1,39 @@
using Kompas6API5;
namespace Kompas.Mcp.Core.Query;
/// <summary>
/// Классификаторы топологических объектов КОМПАС: строковый тип грани и ребра.
/// <para>
/// Каждый вызов COM-метода обёрнут в отдельный <c>try/catch</c> — на экзотической геометрии
/// (NURBS, импортированные STEP-тела) методы класса <c>ksFaceDefinition</c>/<c>ksEdgeDefinition</c>
/// могут бросить <c>COMException</c>, что не должно прерывать bulk-перебор. Ранее этот код
/// был продублирован в <c>QueryService</c> (без защиты) и <c>ModelInspectionService</c> (с защитой).
/// </para>
/// </summary>
internal static class GeomClassifiers
{
/// <summary>Строковый тип поверхности: plane/cylinder/cone/sphere/torus/nurbs/other.</summary>
internal 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";
}
/// <summary>Строковый тип кривой: line/circle/arc/ellipse_arc/ellipse/nurbs/other.</summary>
internal 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";
}
}
+10 -6
View File
@@ -23,8 +23,9 @@ public static class InspectionText
}
// Формообразующие операции, наличие которых означает параметрическую историю.
private static readonly string[] OpKinds =
{ "sketch", "extrusion", "cut_extrusion", "revolve", "cut_revolve", "fillet", "chamfer" };
// HashSet<string> даёт O(1) Contains вместо O(n) по массиву при bulk-проверке граней.
private static readonly HashSet<string> OpKinds =
new(StringComparer.Ordinal) { "sketch", "extrusion", "cut_extrusion", "revolve", "cut_revolve", "fillet", "chamfer" };
/// <summary>
/// Модель — «голый» импорт без истории построения (типично для STEP): в дереве только начало
@@ -39,18 +40,21 @@ public static class InspectionText
public static string GroupTypes(IReadOnlyList<string> types)
{
if (types.Count == 0) return "—";
// Count материализуем один раз: IGrouping<K,V> — IEnumerable без кеша,
// двойной вызов g.Count() итерирует группу дважды.
return string.Join(", ", types
.GroupBy(t => t)
.OrderByDescending(g => g.Count())
.ThenBy(g => g.Key, StringComparer.Ordinal)
.Select(g => $"{g.Count()} {g.Key}"));
.Select(g => (Key: g.Key, Count: g.Count()))
.OrderByDescending(x => x.Count)
.ThenBy(x => x.Key, StringComparer.Ordinal)
.Select(x => $"{x.Count} {x.Key}"));
}
/// <summary>Отрендерить «паспорт» модели в читаемый текст.</summary>
public static string Render(ModelDescription d)
{
var sb = new StringBuilder();
sb.AppendLine($"Документ: «{d.Name}» (деталь)");
sb.AppendLine($"Документ: «{d.Name}» ({d.DocumentTypeName})");
if (d.IsEmpty)
{
@@ -38,4 +38,10 @@ public sealed record ModelDescription
/// <summary>Переменные модели.</summary>
public required IReadOnlyList<VariableInfo> Variables { get; init; }
/// <summary>
/// Локализованное название типа документа для вывода: «деталь», «сборка», «чертёж» и т.д.
/// По умолчанию «деталь».
/// </summary>
public string DocumentTypeName { get; init; } = "деталь";
}
@@ -1,6 +1,8 @@
using System.Globalization;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Documents;
using Kompas.Mcp.Core.Interop;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using Kompas6Constants3D;
@@ -34,7 +36,7 @@ public sealed class ModelInspectionService
var part = GetTopPart();
try
{
var name = ActiveDocName();
var (name, typeName) = ActiveDocInfo();
bool hasGab = part.GetGabarit(false, false,
out double x1, out double y1, out double z1, out double x2, out double y2, out double z2);
@@ -48,6 +50,7 @@ public sealed class ModelInspectionService
Bodies = Array.Empty<BodyInfo>(), FaceTypes = Array.Empty<string>(),
EdgeTypes = Array.Empty<string>(), Features = Array.Empty<FeatureInfo>(),
Imported = false, Variables = Array.Empty<VariableInfo>(),
DocumentTypeName = typeName,
};
var bodies = ReadBodies(part);
@@ -67,9 +70,10 @@ public sealed class ModelInspectionService
Features = features,
Imported = InspectionText.IsImported(features, bodies.Count),
Variables = variables,
DocumentTypeName = typeName,
};
}
finally { Release(part); }
finally { ComHelper.Release(part); }
}, ct);
/// <summary>Перечислить узлы дерева построения по порядку.</summary>
@@ -78,7 +82,7 @@ public sealed class ModelInspectionService
{
var part = GetTopPart();
try { return ReadFeatures(part); }
finally { Release(part); }
finally { ComHelper.Release(part); }
}, ct);
/// <summary>Перечислить тела детали.</summary>
@@ -87,7 +91,7 @@ public sealed class ModelInspectionService
{
var part = GetTopPart();
try { return ReadBodies(part); }
finally { Release(part); }
finally { ComHelper.Release(part); }
}, ct);
/// <summary>Перечислить переменные модели.</summary>
@@ -96,7 +100,7 @@ public sealed class ModelInspectionService
{
var part = GetTopPart();
try { return ReadVariables(part); }
finally { Release(part); }
finally { ComHelper.Release(part); }
}, ct);
/// <summary>Детали грани по индексу из list_faces: тип, площадь, нормаль, радиус, число рёбер.</summary>
@@ -117,14 +121,14 @@ public sealed class ModelInspectionService
return new FaceDetail
{
Index = index,
Type = FaceType(def),
Type = GeomClassifiers.FaceType(def),
Area = area,
Normal = FaceNormal(def),
Radius = radius,
EdgeCount = edgeCount,
};
}
finally { Release(part); }
finally { ComHelper.Release(part); }
}, ct);
/// <summary>Детали ребра по индексу из list_edges: тип, длина, смежные грани, концевые вершины.</summary>
@@ -139,13 +143,13 @@ public sealed class ModelInspectionService
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 { }
try { if (def.GetAdjacentFace(true) is ksFaceDefinition a) f1 = GeomClassifiers.FaceType(a); } catch { }
try { if (def.GetAdjacentFace(false) is ksFaceDefinition b) f2 = GeomClassifiers.FaceType(b); } catch { }
return new EdgeDetail
{
Index = index,
Type = EdgeType(def),
Type = GeomClassifiers.EdgeType(def),
Length = len,
AdjacentFace1 = f1,
AdjacentFace2 = f2,
@@ -153,7 +157,7 @@ public sealed class ModelInspectionService
End = Vertex(def, false),
};
}
finally { Release(part); }
finally { ComHelper.Release(part); }
}, ct);
/// <summary>
@@ -186,7 +190,7 @@ public sealed class ModelInspectionService
AngleValid = angleValid,
};
}
finally { Release(part); }
finally { ComHelper.Release(part); }
}, ct);
// ── чтение данных (на STA-потоке) ─────────────────────────────────────
@@ -264,11 +268,11 @@ public sealed class ModelInspectionService
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),
Name = ComHelper.SafeStr(() => v.name),
Expression = ComHelper.SafeStr(() => v.Expression),
Value = ComHelper.SafeNum(() => v.value),
External = ComHelper.SafeBool(() => v.external),
Information = ComHelper.SafeBool(() => v.Information),
});
}
return list;
@@ -280,10 +284,12 @@ public sealed class ModelInspectionService
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 (faces.GetByIndex(i) is ksEntity e && e.GetDefinition() is ksFaceDefinition d)
faceTypes.Add(GeomClassifiers.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));
if (edges.GetByIndex(i) is ksEntity e && e.GetDefinition() is ksEdgeDefinition d)
edgeTypes.Add(GeomClassifiers.EdgeType(d));
return (faceTypes, edgeTypes);
}
@@ -349,16 +355,43 @@ public sealed class ModelInspectionService
{
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).");
// Освобождаем RCW документа сразу после получения ksPart — part имеет собственный ref.
try
{
return doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
}
finally { ComHelper.Release(doc3d); }
}
private string ActiveDocName()
/// <summary>Имя и локализованный тип активного документа. Отдельный API7-вызов от GetTopPart.</summary>
private (string name, string typeName) ActiveDocInfo()
{
var doc = _session.Application.ActiveDocument;
try { return doc?.Name ?? ""; }
catch { return ""; }
finally { Release(doc); }
try
{
string name = ComHelper.SafeStr(() => doc?.Name);
string typeName = "деталь";
if (doc is not null)
{
try
{
typeName = DocumentTypes.FromEnum(doc.DocumentType) switch
{
KompasDocumentType.Assembly => "сборка",
KompasDocumentType.Drawing => "чертёж",
KompasDocumentType.Fragment => "фрагмент",
KompasDocumentType.Specification => "спецификация",
KompasDocumentType.Text => "текст",
_ => "деталь",
};
}
catch { }
}
return (name, typeName);
}
catch { return ("", "деталь"); }
finally { ComHelper.Release(doc); }
}
private static ksEntityCollection? Faces(ksPart part) => part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection;
@@ -382,44 +415,11 @@ public sealed class ModelInspectionService
?? 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 { }
}
}
}
+46 -72
View File
@@ -1,5 +1,6 @@
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Kompas.Mcp.Core.Interop;
using Kompas.Mcp.Core.Threading;
using Kompas6API5;
using Kompas6Constants3D;
@@ -14,6 +15,9 @@ public sealed class QueryService
// Иначе КОМПАС вернёт значения в единицах по умолчанию (см/г).
private const int MixMmKg = 0x1 | 0x10;
// ST_MIX_MM (0x1) — площади граней / длины рёбер в мм.
private const uint MixMm = 0x1;
private readonly KompasSession _session;
private readonly KompasDispatcher _dispatcher;
@@ -33,11 +37,7 @@ public sealed class QueryService
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).");
var part = GetTopPart();
ksMassInertiaParam? mass = null;
try
{
@@ -56,8 +56,8 @@ public sealed class QueryService
}
finally
{
if (mass is not null && Marshal.IsComObject(mass)) Marshal.ReleaseComObject(mass);
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
ComHelper.Release(mass);
ComHelper.Release(part);
}
}, ct);
@@ -83,13 +83,22 @@ public sealed class QueryService
bool gab = false;
try { gab = p.GetGabarit(false, false, out x1, out y1, out z1, out x2, out y2, out z2); }
catch { /* габарит может быть недоступен у отдельных компонентов */ }
// Inline safe-геттеры: избегаем лямбда-аллокаций в горячем цикле по компонентам.
string name = "", marking = "", fileName = "";
bool isDetail = false;
try { name = p.Name ?? ""; } catch { }
try { marking = p.Marking ?? ""; } catch { }
try { fileName = p.FileName ?? ""; } catch { }
try { isDetail = p.Detail; } catch { }
list.Add(new ComponentInfo
{
Index = i,
Name = SafeGet(() => p.Name),
Marking = SafeGet(() => p.Marking),
FileName = SafeGet(() => p.FileName),
IsDetail = SafeBool(() => p.Detail),
Name = name,
Marking = marking,
FileName = fileName,
IsDetail = isDetail,
SizeX = gab ? Math.Abs(x2 - x1) : 0,
SizeY = gab ? Math.Abs(y2 - y1) : 0,
SizeZ = gab ? Math.Abs(z2 - z1) : 0,
@@ -98,27 +107,11 @@ public sealed class QueryService
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).");
var part = GetTopPart();
try
{
var faces = part.EntityCollection((short)Obj3dType.o3d_face) as ksEntityCollection
@@ -133,37 +126,20 @@ public sealed class QueryService
list.Add(new FaceInfo
{
Index = i,
Type = FaceType(def),
Type = GeomClassifiers.FaceType(def),
Area = def.GetArea(MixMm),
});
}
return list;
}
finally
{
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
}
finally { ComHelper.Release(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).");
var part = GetTopPart();
try
{
var edges = part.EntityCollection((short)Obj3dType.o3d_edge) as ksEntityCollection
@@ -178,37 +154,20 @@ public sealed class QueryService
list.Add(new EdgeInfo
{
Index = i,
Type = EdgeType(def),
Type = GeomClassifiers.EdgeType(def),
Length = def.GetLength(MixMm),
});
}
return list;
}
finally
{
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
}
finally { ComHelper.Release(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).");
var part = GetTopPart();
try
{
// full=false → только тела (без вспомогательной геометрии); customizable игнорируется.
@@ -223,9 +182,24 @@ public sealed class QueryService
MaxX = Math.Max(x1, x2), MaxY = Math.Max(y1, y2), MaxZ = Math.Max(z1, z2),
};
}
finally
{
if (Marshal.IsComObject(part)) Marshal.ReleaseComObject(part);
}
finally { ComHelper.Release(part); }
}, ct);
// ── низкоуровневые помощники ──────────────────────────────────────────
/// <summary>
/// Получить вершинный компонент активной детали. Освобождает RCW doc3d сразу после
/// GetPart — ksPart имеет собственный COM-счётчик ссылок.
/// </summary>
private ksPart GetTopPart()
{
var doc3d = _session.Kompas.ActiveDocument3D() as ksDocument3D
?? throw new InvalidOperationException("Нет активного 3D-документа.");
try
{
return doc3d.GetPart((short)Part_Type.pTop_Part) as ksPart
?? throw new InvalidOperationException("Не удалось получить вершинный компонент (ksPart).");
}
finally { ComHelper.Release(doc3d); }
}
}
@@ -25,6 +25,18 @@ public static class RasterImageFormats
_ => "application/octet-stream",
};
/// <summary>Расширение файла без точки — постоянная строка без аллокации (в отличие от ToString().ToLower()).</summary>
public static string FileExtension(this RasterImageFormat format) => format switch
{
RasterImageFormat.Bmp => "bmp",
RasterImageFormat.Gif => "gif",
RasterImageFormat.Jpg => "jpg",
RasterImageFormat.Png => "png",
RasterImageFormat.Tif => "tif",
RasterImageFormat.Tga => "tga",
_ => format.ToString().ToLowerInvariant(),
};
public static RasterImageFormat Parse(string value) => value.Trim().ToLowerInvariant() switch
{
"png" => RasterImageFormat.Png,
@@ -64,7 +64,7 @@ public sealed class SnapshotService
param.returnResultAsArrayBytes = false;
var outFile = saveToFile ?? Path.Combine(
Path.GetTempPath(), $"kompas-snap-{Guid.NewGuid():N}.{format.ToString().ToLowerInvariant()}");
Path.GetTempPath(), $"kompas-snap-{Guid.NewGuid():N}.{format.FileExtension()}");
var deleteAfter = saveToFile is null;
try